Support isolated actuator ObjectMapper

Closes gh-20291
pull/33098/head
Phillip Webb 2 years ago
commit 12c77dba1c

@ -27,6 +27,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
@ -81,7 +82,7 @@ public class ConditionsReportEndpoint {
/**
* A description of an application's condition evaluation.
*/
public static final class ConditionsDescriptor {
public static final class ConditionsDescriptor implements OperationResponseBody {
private final Map<String, ContextConditionsDescriptor> contexts;

@ -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;
}
}

@ -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;

@ -23,6 +23,10 @@ import java.util.HashSet;
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;
@ -35,7 +39,9 @@ import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnMa
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;
@ -53,6 +59,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
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;
@ -98,6 +105,13 @@ class JerseyWebEndpointManagementContextConfiguration {
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)
@ -192,4 +206,24 @@ class JerseyWebEndpointManagementContextConfiguration {
}
/**
* {@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;
}
}
}

@ -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.
@ -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;
@ -28,7 +35,9 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
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;
@ -48,7 +57,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
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;
@ -114,4 +130,55 @@ public class WebFluxEndpointManagementContextConfiguration {
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));
});
}
}
}
}

@ -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;
@ -28,7 +33,9 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
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;
@ -49,9 +56,14 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
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
@ -116,4 +128,46 @@ public class WebMvcEndpointManagementContextConfiguration {
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));
});
}
}
}

@ -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"

@ -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

@ -0,0 +1,76 @@
/*
* 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.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JacksonEndpointAutoConfiguration}.
*
* @author Phillip Webb
*/
class JacksonEndpointAutoConfigurationTests {
private ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonEndpointAutoConfiguration.class));
@Test
void endpointObjectMapperWhenNoProperty() {
this.runner.run((context) -> assertThat(context).hasSingleBean(EndpointObjectMapper.class));
}
@Test
void endpointObjectMapperWhenPropertyTrue() {
this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=true")
.run((context) -> assertThat(context).hasSingleBean(EndpointObjectMapper.class));
}
@Test
void endpointObjectMapperWhenPropertyFalse() {
this.runner.withPropertyValues("management.endpoints.jackson.isolated-object-mapper=false")
.run((context) -> assertThat(context).doesNotHaveBean(EndpointObjectMapper.class));
}
@Configuration(proxyBeanMethods = false)
static class TestEndpointMapperConfiguration {
@Bean
TestEndpointObjectMapper testEndpointObjectMapper() {
return new TestEndpointObjectMapper();
}
}
static class TestEndpointObjectMapper implements EndpointObjectMapper {
@Override
public ObjectMapper get() {
return null;
}
}
}

@ -0,0 +1,79 @@
/*
* 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.integrationtest;
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.StdScalarSerializer;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
/**
* {@link Configuration @Configuration} that creates an {@link EndpointObjectMapper} that
* reverses all strings.
*
* @author Phillip Webb
*/
@Configuration
class EndpointObjectMapperConfiguration {
@Bean
EndpointObjectMapper endpointObjectMapper() {
SimpleModule module = new SimpleModule();
module.addSerializer(String.class, new ReverseStringSerializer());
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder.json().modules(module).build();
return () -> objectMapper;
}
static class ReverseStringSerializer extends StdScalarSerializer<Object> {
ReverseStringSerializer() {
super(String.class, false);
}
@Override
public boolean isEmpty(SerializerProvider prov, Object value) {
return ((String) value).isEmpty();
}
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
serialize(value, gen);
}
@Override
public final void serializeWithType(Object value, JsonGenerator gen, SerializerProvider provider,
TypeSerializer typeSer) throws IOException {
serialize(value, gen);
}
private void serialize(Object value, JsonGenerator gen) throws IOException {
StringBuilder builder = new StringBuilder((String) value);
gen.writeString(builder.reverse().toString());
}
}
}

@ -16,6 +16,7 @@
package org.springframework.boot.actuate.autoconfigure.integrationtest;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
@ -44,6 +45,8 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the Jersey actuator endpoints.
*
@ -87,7 +90,22 @@ class JerseyEndpointIntegrationTests {
.responseTimeout(Duration.ofMinutes(5)).build();
client.get().uri("/actuator").exchange().expectStatus().isUnauthorized();
});
}
@Test
void endpointObjectMapperCanBeApplied() {
WebApplicationContextRunner contextRunner = getContextRunner(new Class<?>[] { EndpointsConfiguration.class,
ResourceConfigConfiguration.class, EndpointObjectMapperConfiguration.class });
contextRunner.run((context) -> {
int port = context.getSourceApplicationContext(AnnotationConfigServletWebServerApplicationContext.class)
.getWebServer().getPort();
WebTestClient client = WebTestClient.bindToServer().baseUrl("http://localhost:" + port)
.responseTimeout(Duration.ofMinutes(5)).build();
client.get().uri("/actuator/beans").exchange().expectStatus().isOk().expectBody().consumeWith((result) -> {
String json = new String(result.getResponseBody(), StandardCharsets.UTF_8);
assertThat(json).contains("\"scope\":\"notelgnis\"");
});
});
}
protected void testJerseyEndpoints(Class<?>[] userConfigurations) {

@ -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.
@ -16,6 +16,8 @@
package org.springframework.boot.actuate.autoconfigure.integrationtest;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration;
@ -36,6 +38,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for the WebFlux actuator endpoints.
*
@ -68,6 +72,19 @@ class WebFluxEndpointIntegrationTests {
});
}
@Test
void endpointObjectMapperCanBeApplied() {
this.contextRunner.withUserConfiguration(EndpointObjectMapperConfiguration.class)
.withPropertyValues("management.endpoints.web.exposure.include:*").run((context) -> {
WebTestClient client = createWebTestClient(context);
client.get().uri("/actuator/beans").exchange().expectStatus().isOk().expectBody()
.consumeWith((result) -> {
String json = new String(result.getResponseBody(), StandardCharsets.UTF_8);
assertThat(json).contains("\"scope\":\"notelgnis\"");
});
});
}
private WebTestClient createWebTestClient(ApplicationContext context) {
return WebTestClient.bindToApplicationContext(context).configureClient().baseUrl("https://spring.example.org")
.build();

@ -52,6 +52,7 @@ import org.springframework.mock.web.MockServletContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.DefaultMockMvcBuilder;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.setup.MockMvcConfigurer;
@ -140,6 +141,16 @@ class WebMvcEndpointIntegrationTests {
mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isNotFound());
}
@Test
void endpointObjectMapperCanBeApplied() throws Exception {
this.context = new AnnotationConfigServletWebApplicationContext();
this.context.register(EndpointObjectMapperConfiguration.class, DefaultConfiguration.class);
TestPropertyValues.of("management.endpoints.web.exposure.include=*").applyTo(this.context);
MockMvc mockMvc = doCreateMockMvc();
MvcResult result = mockMvc.perform(get("/actuator/beans")).andExpect(status().isOk()).andReturn();
assertThat(result.getResponse().getContentAsString()).contains("\"scope\":\"notelgnis\"");
}
private MockMvc createSecureMockMvc() {
return doCreateMockMvc(springSecurity());
}

@ -20,6 +20,7 @@ import java.time.Instant;
import java.time.OffsetDateTime;
import java.util.List;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.lang.Nullable;
@ -55,7 +56,7 @@ public class AuditEventsEndpoint {
/**
* Description of an application's {@link AuditEvent audit events}.
*/
public static final class AuditEventsDescriptor {
public static final class AuditEventsDescriptor implements OperationResponseBody {
private final List<AuditEvent> events;

@ -22,6 +22,7 @@ import java.util.Map;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.ApplicationContext;
@ -73,7 +74,7 @@ public class BeansEndpoint {
/**
* Description of an application's beans.
*/
public static final class BeansDescriptor {
public static final class BeansDescriptor implements OperationResponseBody {
private final Map<String, ContextBeansDescriptor> contexts;

@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Objects;
import java.util.function.Predicate;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -148,7 +149,7 @@ public class CachesEndpoint {
/**
* Description of the caches.
*/
public static final class CachesDescriptor {
public static final class CachesDescriptor implements OperationResponseBody {
private final Map<String, CacheManagerDescriptor> cacheManagers;
@ -182,7 +183,7 @@ public class CachesEndpoint {
/**
* Description of a {@link Cache}.
*/
public static class CacheDescriptor {
public static class CacheDescriptor implements OperationResponseBody {
private final String target;

@ -17,6 +17,7 @@
package org.springframework.boot.actuate.context;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.context.ApplicationContext;
@ -71,7 +72,7 @@ public class ShutdownEndpoint implements ApplicationContextAware {
/**
* Description of the shutdown.
*/
public static class ShutdownDescriptor {
public static class ShutdownDescriptor implements OperationResponseBody {
private static final ShutdownDescriptor DEFAULT = new ShutdownDescriptor("Shutting down, bye...");

@ -54,6 +54,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
@ -564,7 +565,7 @@ public class ConfigurationPropertiesReportEndpoint implements ApplicationContext
* Description of an application's
* {@link ConfigurationProperties @ConfigurationProperties} beans.
*/
public static final class ConfigurationPropertiesDescriptor {
public static final class ConfigurationPropertiesDescriptor implements OperationResponseBody {
private final Map<String, ContextConfigurationPropertiesDescriptor> contexts;

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 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.
@ -32,7 +32,9 @@ public interface Operation {
OperationType getType();
/**
* Invoke the underlying operation using the given {@code context}.
* Invoke the underlying operation using the given {@code context}. Results intended
* to be returned in the body of the response should additionally implement
* {@link OperationResponseBody}.
* @param context the context in to use when invoking the operation
* @return the result of the operation, may be {@code null}
*/

@ -0,0 +1,45 @@
/*
* 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.endpoint;
import java.util.Map;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Tagging interface used to indicate that an operation result is intended to be returned
* in the body of the response. Primarily intended to support JSON serialzation using an
* endpoint specific {@link ObjectMapper}.
*
* @author Phillip Webb
* @since 3.0.0
*/
public interface OperationResponseBody {
/**
* Return a {@link OperationResponseBody} {@link Map} instance containing entires from
* the given {@code map}.
* @param <K> the key type
* @param <V> the value type
* @param map the source map or {@code null}
* @return a {@link OperationResponseBody} version of the map or {@code null}
*/
static <K, V> Map<K, V> of(Map<K, V> map) {
return (map != null) ? new OperationResponseBodyMap<>(map) : null;
}
}

@ -0,0 +1,35 @@
/*
* 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.endpoint;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* {@link LinkedHashMap} to support {@link OperationResponseBody#of(java.util.Map)}.
*
* @param <K> the key type
* @param <V> the value type
* @author Phillip Webb
*/
class OperationResponseBodyMap<K, V> extends LinkedHashMap<K, V> implements OperationResponseBody {
OperationResponseBodyMap(Map<? extends K, ? extends V> map) {
super(map);
}
}

@ -0,0 +1,40 @@
/*
* 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.endpoint.jackson;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
/**
* Interface used to supply the {@link ObjectMapper} that should be used when serializing
* {@link OperationResponseBody} endpoint results.
*
* @author Phillip Webb
* @since 3.0.0
* @see OperationResponseBody
*/
public interface EndpointObjectMapper {
/**
* Return the {@link ObjectMapper} that should be used to serialize
* {@link OperationResponseBody} endpoint results.
* @return the object mapper
*/
ObjectMapper get();
}

@ -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.
*/
/**
* Jackson support classes for actuator endpoints.
*/
package org.springframework.boot.actuate.endpoint.jackson;

@ -43,6 +43,7 @@ import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
@ -323,7 +324,8 @@ public class JerseyEndpointResourceFactory {
public Response apply(ContainerRequestContext request) {
Map<String, Link> links = this.linksResolver
.resolveLinks(request.getUriInfo().getAbsolutePath().toString());
return Response.ok(Collections.singletonMap("_links", links)).build();
Map<String, Map<String, Link>> entity = OperationResponseBody.of(Collections.singletonMap("_links", links));
return Response.ok(entity).build();
}
}

@ -26,6 +26,7 @@ import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -87,8 +88,8 @@ public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandle
public Map<String, Map<String, Link>> links(ServerWebExchange exchange) {
String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI()).replaceQuery(null)
.toUriString();
return Collections.singletonMap("_links",
WebFluxEndpointHandlerMapping.this.linksResolver.resolveLinks(requestUri));
Map<String, Link> links = WebFluxEndpointHandlerMapping.this.linksResolver.resolveLinks(requestUri);
return OperationResponseBody.of(Collections.singletonMap("_links", links));
}
@Override

@ -28,6 +28,7 @@ import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -84,8 +85,9 @@ public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerM
@ResponseBody
@Reflective
public Map<String, Map<String, Link>> links(HttpServletRequest request, HttpServletResponse response) {
return Collections.singletonMap("_links",
WebMvcEndpointHandlerMapping.this.linksResolver.resolveLinks(request.getRequestURL().toString()));
Map<String, Link> links = WebMvcEndpointHandlerMapping.this.linksResolver
.resolveLinks(request.getRequestURL().toString());
return OperationResponseBody.of(Collections.singletonMap("_links", links));
}
@Override

@ -27,6 +27,7 @@ import java.util.stream.Stream;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
@ -203,7 +204,7 @@ public class EnvironmentEndpoint {
/**
* Description of an {@link Environment}.
*/
public static final class EnvironmentDescriptor {
public static final class EnvironmentDescriptor implements OperationResponseBody {
private final List<String> activeProfiles;

@ -28,6 +28,7 @@ import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationInfo;
import org.flywaydb.core.api.MigrationState;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.ApplicationContext;
@ -69,7 +70,7 @@ public class FlywayEndpoint {
/**
* Description of an application's {@link Flyway} beans.
*/
public static final class FlywayBeansDescriptor {
public static final class FlywayBeansDescriptor implements OperationResponseBody {
private final Map<String, ContextFlywayBeansDescriptor> contexts;

@ -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.
@ -18,6 +18,8 @@ package org.springframework.boot.actuate.health;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
/**
* A component that contributes data to results returned from the {@link HealthEndpoint}.
*
@ -26,7 +28,7 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped;
* @see Health
* @see CompositeHealth
*/
public abstract class HealthComponent {
public abstract class HealthComponent implements OperationResponseBody {
HealthComponent() {
}

@ -19,6 +19,7 @@ package org.springframework.boot.actuate.info;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert;
@ -51,7 +52,7 @@ public class InfoEndpoint {
for (InfoContributor contributor : this.infoContributors) {
contributor.contribute(builder);
}
return builder.build().getDetails();
return OperationResponseBody.of(builder.build().getDetails());
}
}

@ -22,6 +22,7 @@ import java.util.Map;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
@ -99,7 +100,7 @@ public class IntegrationGraphEndpoint {
/**
* Description of a {@link Graph}.
*/
public static class GraphDescriptor {
public static class GraphDescriptor implements OperationResponseBody {
private final Map<String, Object> contentDescriptor;

@ -32,6 +32,7 @@ import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.integration.spring.SpringLiquibase;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.ApplicationContext;
@ -106,7 +107,7 @@ public class LiquibaseEndpoint {
/**
* Description of an application's {@link SpringLiquibase} beans.
*/
public static final class LiquibaseBeansDescriptor {
public static final class LiquibaseBeansDescriptor implements OperationResponseBody {
private final Map<String, ContextLiquibaseBeansDescriptor> contexts;

@ -25,6 +25,7 @@ import java.util.Set;
import java.util.TreeSet;
import org.springframework.aot.hint.annotation.RegisterReflectionForBinding;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
@ -121,7 +122,7 @@ public class LoggersEndpoint {
/**
* Description of loggers.
*/
public static class LoggersDescriptor {
public static class LoggersDescriptor implements OperationResponseBody {
/**
* Empty description.
@ -158,7 +159,7 @@ public class LoggersEndpoint {
/**
* Description of levels configured for a given logger.
*/
public static class LoggerLevelsDescriptor {
public static class LoggerLevelsDescriptor implements OperationResponseBody {
private String configuredLevel;

@ -22,6 +22,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.function.Function;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -54,7 +55,7 @@ public class ThreadDumpEndpoint {
/**
* Description of a thread dump.
*/
public static final class ThreadDumpDescriptor {
public static final class ThreadDumpDescriptor implements OperationResponseBody {
private final List<ThreadInfo> threads;

@ -34,6 +34,7 @@ import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.composite.CompositeMeterRegistry;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
@ -159,7 +160,7 @@ public class MetricsEndpoint {
/**
* Description of metric names.
*/
public static final class MetricNamesDescriptor {
public static final class MetricNamesDescriptor implements OperationResponseBody {
private final Set<String> names;
@ -176,7 +177,7 @@ public class MetricsEndpoint {
/**
* Description of a metric.
*/
public static final class MetricDescriptor {
public static final class MetricDescriptor implements OperationResponseBody {
private final String name;

@ -47,6 +47,7 @@ import org.quartz.Trigger.TriggerState;
import org.quartz.TriggerKey;
import org.quartz.impl.matchers.GroupMatcher;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.SanitizableData;
import org.springframework.boot.actuate.endpoint.Sanitizer;
import org.springframework.boot.actuate.endpoint.SanitizingFunction;
@ -236,8 +237,9 @@ public class QuartzEndpoint {
return null;
}
TriggerState triggerState = this.scheduler.getTriggerState(triggerKey);
return TriggerDescriptor.of(trigger).buildDetails(triggerState,
sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized));
TriggerDescriptor triggerDescriptor = TriggerDescriptor.of(trigger);
Map<String, Object> jobDataMap = sanitizeJobDataMap(trigger.getJobDataMap(), showUnsanitized);
return OperationResponseBody.of(triggerDescriptor.buildDetails(triggerState, jobDataMap));
}
private static Duration getIntervalDuration(long amount, IntervalUnit unit) {
@ -279,7 +281,7 @@ public class QuartzEndpoint {
/**
* Description of available job and trigger group names.
*/
public static final class QuartzDescriptor {
public static final class QuartzDescriptor implements OperationResponseBody {
private final GroupNamesDescriptor jobs;
@ -320,7 +322,7 @@ public class QuartzEndpoint {
/**
* Description of each group identified by name.
*/
public static class QuartzGroupsDescriptor {
public static class QuartzGroupsDescriptor implements OperationResponseBody {
private final Map<String, Object> groups;
@ -337,7 +339,7 @@ public class QuartzEndpoint {
/**
* Description of the {@link JobDetail jobs} in a given group.
*/
public static final class QuartzJobGroupSummaryDescriptor {
public static final class QuartzJobGroupSummaryDescriptor implements OperationResponseBody {
private final String group;
@ -382,7 +384,7 @@ public class QuartzEndpoint {
/**
* Description of a {@link Job Quartz Job}.
*/
public static final class QuartzJobDetailsDescriptor {
public static final class QuartzJobDetailsDescriptor implements OperationResponseBody {
private final String group;
@ -449,7 +451,7 @@ public class QuartzEndpoint {
/**
* Description of the {@link Trigger triggers} in a given group.
*/
public static final class QuartzTriggerGroupSummaryDescriptor {
public static final class QuartzTriggerGroupSummaryDescriptor implements OperationResponseBody {
private final String group;

@ -29,6 +29,7 @@ import java.util.stream.Collectors;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.scheduling.ScheduledTasksEndpoint.ScheduledTasksEndpointRuntimeHints;
@ -75,7 +76,7 @@ public class ScheduledTasksEndpoint {
/**
* Description of an application's scheduled {@link Task Tasks}.
*/
public static final class ScheduledTasksDescriptor {
public static final class ScheduledTasksDescriptor implements OperationResponseBody {
private final List<TaskDescriptor> cron;

@ -21,6 +21,7 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
@ -70,7 +71,7 @@ public class SessionsEndpoint {
/**
* Description of user's {@link Session sessions}.
*/
public static final class SessionsDescriptor {
public static final class SessionsDescriptor implements OperationResponseBody {
private final List<SessionDescriptor> sessions;
@ -87,7 +88,7 @@ public class SessionsEndpoint {
/**
* Description of user's {@link Session session}.
*/
public static final class SessionDescriptor {
public static final class SessionDescriptor implements OperationResponseBody {
private final String id;

@ -21,6 +21,7 @@ import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeReference;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
@ -68,7 +69,7 @@ public class StartupEndpoint {
/**
* Description of an application startup.
*/
public static final class StartupDescriptor {
public static final class StartupDescriptor implements OperationResponseBody {
private final String springBootVersion;

@ -18,6 +18,7 @@ package org.springframework.boot.actuate.web.exchanges;
import java.util.List;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.util.Assert;
@ -51,7 +52,7 @@ public class HttpExchangesEndpoint {
/**
* Description of an application's {@link HttpExchange} entries.
*/
public static final class HttpExchangesDescriptor {
public static final class HttpExchangesDescriptor implements OperationResponseBody {
private final List<HttpExchange> exchanges;

@ -20,6 +20,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.context.ApplicationContext;
@ -64,7 +65,7 @@ public class MappingsEndpoint {
/**
* Description of an application's request mappings.
*/
public static final class ApplicationMappingsDescriptor {
public static final class ApplicationMappingsDescriptor implements OperationResponseBody {
private final Map<String, ContextMappingsDescriptor> contextMappings;

@ -0,0 +1,44 @@
/*
* 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.endpoint;
import java.util.LinkedHashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link OperationResponseBody}.
*
* @author Phillip Webb
*/
class OperationResponseBodyTests {
@Test
void ofMapReturnsOperationResponseBody() {
LinkedHashMap<String, String> map = new LinkedHashMap<>();
map.put("one", "1");
map.put("two", "2");
Map<String, String> mapDescriptor = OperationResponseBody.of(map);
assertThat(mapDescriptor).containsExactly(entry("one", "1"), entry("two", "2"));
assertThat(mapDescriptor).isInstanceOf(OperationResponseBody.class);
}
}

@ -68,19 +68,19 @@ class IntegrationGraphEndpointTests {
@Test
void readOperationShouldReturnGraph() {
Graph mockedGraph = mock(Graph.class);
Graph graph = mock(Graph.class);
Map<String, Object> contentDescriptor = new LinkedHashMap<>();
Collection<IntegrationNode> nodes = new ArrayList<>();
Collection<LinkNode> links = new ArrayList<>();
given(mockedGraph.getContentDescriptor()).willReturn(contentDescriptor);
given(mockedGraph.getNodes()).willReturn(nodes);
given(mockedGraph.getLinks()).willReturn(links);
given(this.server.getGraph()).willReturn(mockedGraph);
GraphDescriptor graph = this.endpoint.graph();
given(graph.getContentDescriptor()).willReturn(contentDescriptor);
given(graph.getNodes()).willReturn(nodes);
given(graph.getLinks()).willReturn(links);
given(this.server.getGraph()).willReturn(graph);
GraphDescriptor descriptor = this.endpoint.graph();
then(this.server).should().getGraph();
assertThat(graph.getContentDescriptor()).isSameAs(contentDescriptor);
assertThat(graph.getNodes()).isSameAs(nodes);
assertThat(graph.getLinks()).isSameAs(links);
assertThat(descriptor.getContentDescriptor()).isSameAs(contentDescriptor);
assertThat(descriptor.getNodes()).isSameAs(nodes);
assertThat(descriptor.getLinks()).isSameAs(links);
}
@Test

@ -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.
@ -25,6 +25,7 @@ import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthContributor;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
@ -32,10 +33,6 @@ import org.springframework.context.annotation.Bean;
@ConfigurationPropertiesScan
public class SampleActuatorApplication {
public static void main(String[] args) {
SpringApplication.run(SampleActuatorApplication.class, args);
}
@Bean
public HealthIndicator helloHealthIndicator() {
return createHealthIndicator("world");
@ -61,4 +58,10 @@ public class SampleActuatorApplication {
return () -> Health.up().withDetail("hello", value).build();
}
public static void main(String[] args) {
SpringApplication application = new SpringApplication(SampleActuatorApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(1024));
application.run(args);
}
}

@ -28,3 +28,5 @@ management.endpoint.health.group.comp.show-details=always
management.endpoints.migrate-legacy-ids=true
management.endpoints.jackson.isolated-object-mapper=true
spring.jackson.visibility.field=any

@ -0,0 +1,32 @@
/*
* 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 smoketest.actuator;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.test.context.SpringBootContextLoader;
class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader {
@Override
protected SpringApplication getSpringApplication() {
SpringApplication application = new SpringApplication();
application.setApplicationStartup(new BufferingApplicationStartup(1024));
return application;
}
}

@ -0,0 +1,52 @@
/*
* 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 smoketest.actuator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for WebMVC actuator when using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=false")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
class SampleActuatorApplicationIsolatedObjectMapperFalseTests {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void resourceShouldBeAvailableOnMainPort() {
ResponseEntity<String> entity = this.testRestTemplate.withBasicAuth("user", "password")
.getForEntity("/actuator/startup", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@ -0,0 +1,53 @@
/*
* 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 smoketest.actuator;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for WebMVC actuator when using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=true")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
class SampleActuatorApplicationIsolatedObjectMapperTrueTests {
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void resourceShouldBeAvailableOnMainPort() {
ResponseEntity<String> entity = this.testRestTemplate.withBasicAuth("user", "password")
.getForEntity("/actuator/startup", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"timeline\":");
}
}

@ -18,13 +18,15 @@ package smoketest.jersey;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SampleJerseyApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class)).run(args);
new SampleJerseyApplication().configure(new SpringApplicationBuilder(SampleJerseyApplication.class)
.applicationStartup(new BufferingApplicationStartup(2048))).run(args);
}
}

@ -0,0 +1,3 @@
management.endpoints.web.exposure.include=*
management.endpoints.jackson.isolated-object-mapper=true
spring.jackson.visibility.field=any

@ -0,0 +1,32 @@
/*
* 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 smoketest.jersey;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.test.context.SpringBootContextLoader;
class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader {
@Override
protected SpringApplication getSpringApplication() {
SpringApplication application = new SpringApplication();
application.setApplicationStartup(new BufferingApplicationStartup(1024));
return application;
}
}

@ -0,0 +1,62 @@
/*
* 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 smoketest.jersey;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for Jersey actuator when not using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=false")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
public class JerseyActuatorIsolatedObjectMapperFalseTests {
@LocalServerPort
private int port;
@LocalManagementPort
private int managementPort;
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void resourceShouldBeAvailableOnMainPort() {
ResponseEntity<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(entity.getBody())
.contains("Java 8 date/time type `java.time.Clock$SystemClock` not supported by default");
}
}

@ -0,0 +1,61 @@
/*
* 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 smoketest.jersey;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalManagementPort;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for Jersey actuator when using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=true")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
public class JerseyActuatorIsolatedObjectMapperTrueTests {
@LocalServerPort
private int port;
@LocalManagementPort
private int managementPort;
@Autowired
private TestRestTemplate testRestTemplate;
@Test
void resourceShouldBeAvailableOnMainPort() {
ResponseEntity<String> entity = this.testRestTemplate
.getForEntity("http://localhost:" + this.port + "/actuator/startup", String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).contains("\"timeline\":");
}
}

@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 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.
@ -18,6 +18,7 @@ package smoketest.webflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.context.annotation.Bean;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
@ -29,7 +30,9 @@ import static org.springframework.web.reactive.function.server.RouterFunctions.r
public class SampleWebFluxApplication {
public static void main(String[] args) {
SpringApplication.run(SampleWebFluxApplication.class, args);
SpringApplication application = new SpringApplication(SampleWebFluxApplication.class);
application.setApplicationStartup(new BufferingApplicationStartup(1024));
application.run(args);
}
@Bean

@ -0,0 +1,3 @@
management.endpoints.web.exposure.include=*
management.endpoints.jackson.isolated-object-mapper=true
spring.jackson.visibility.field=any

@ -0,0 +1,32 @@
/*
* 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 smoketest.webflux;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.metrics.buffering.BufferingApplicationStartup;
import org.springframework.boot.test.context.SpringBootContextLoader;
class ApplicationStartupSpringBootContextLoader extends SpringBootContextLoader {
@Override
protected SpringApplication getSpringApplication() {
SpringApplication application = new SpringApplication();
application.setApplicationStartup(new BufferingApplicationStartup(1024));
return application;
}
}

@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = { "management.server.port=0", "management.endpoints.web.base-path=/" })
class WebFluxDifferentPortSampleActuatorApplicationTests {
class SampleWebFluxApplicationActuatorDifferentPortTests {
@LocalManagementPort
private int managementPort;

@ -0,0 +1,47 @@
/*
* 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 smoketest.webflux;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Integration test for WebFlux actuator when using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=false")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
class SampleWebFluxApplicationActuatorIsolatedObjectMapperFalseTests {
@Autowired
private WebTestClient webClient;
@Test
void linksEndpointShouldBeAvailable() {
this.webClient.get().uri("/actuator/startup").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.is5xxServerError();
}
}

@ -0,0 +1,57 @@
/*
* 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 smoketest.webflux;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.web.reactive.server.EntityExchangeResult;
import org.springframework.test.web.reactive.server.WebTestClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration test for WebFlux actuator when not using an isolated {@link ObjectMapper}.
*
* @author Phillip Webb
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "management.endpoints.jackson.isolated-object-mapper=true")
@ContextConfiguration(loader = ApplicationStartupSpringBootContextLoader.class)
class SampleWebFluxApplicationActuatorIsolatedObjectMapperTrueTests {
@Autowired
private WebTestClient webClient;
@Test
void linksEndpointShouldBeAvailable() {
this.webClient.get().uri("/actuator/startup").accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.isOk().expectBody().consumeWith(this::assertExpectedJson);
}
private void assertExpectedJson(EntityExchangeResult<byte[]> result) {
String body = new String(result.getResponseBody(), StandardCharsets.UTF_8);
assertThat(body).contains("\"timeline\":");
}
}
Loading…
Cancel
Save