diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java index d32af3609f9..8975941f313 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/EndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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.endpoint.annotation.EndpointConverter; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoker.cache.CachingOperationInvokerAdvisor; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.convert.ApplicationConversionService; @@ -75,4 +76,9 @@ public class EndpointAutoConfiguration { return new CachingOperationInvokerAdvisor(new EndpointIdTimeToLivePropertyFunction(environment)); } + @Bean + public ActuatorJsonMapperProvider actuatorJsonMapperProvider() { + return new ActuatorJsonMapperProvider(); + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java index e2af732276b..b71271a1b7c 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/jmx/JmxEndpointAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -20,8 +20,6 @@ import java.util.stream.Collectors; import javax.management.MBeanServer; -import com.fasterxml.jackson.databind.ObjectMapper; - import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludePropertyEndpointFilter; import org.springframework.boot.actuate.endpoint.EndpointFilter; @@ -35,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointExporter; import org.springframework.boot.actuate.endpoint.jmx.JmxEndpointsSupplier; import org.springframework.boot.actuate.endpoint.jmx.JmxOperationResponseMapper; import org.springframework.boot.actuate.endpoint.jmx.annotation.JmxEndpointDiscoverer; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -85,12 +84,12 @@ public class JmxEndpointAutoConfiguration { @Bean @ConditionalOnSingleCandidate(MBeanServer.class) public JmxEndpointExporter jmxMBeanExporter(MBeanServer mBeanServer, Environment environment, - ObjectProvider objectMapper, JmxEndpointsSupplier jmxEndpointsSupplier) { + ActuatorJsonMapperProvider actuatorJsonMapperProvider, JmxEndpointsSupplier jmxEndpointsSupplier) { String contextId = ObjectUtils.getIdentityHexString(this.applicationContext); EndpointObjectNameFactory objectNameFactory = new DefaultEndpointObjectNameFactory(this.properties, environment, mBeanServer, contextId); JmxOperationResponseMapper responseMapper = new JacksonJmxOperationResponseMapper( - objectMapper.getIfAvailable()); + actuatorJsonMapperProvider.getInstance()); return new JmxEndpointExporter(mBeanServer, objectNameFactory, responseMapper, jmxEndpointsSupplier.getEndpoints()); diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java index d22961a5f62..f6c451d31be 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -22,7 +22,10 @@ import java.util.HashSet; import java.util.List; import javax.annotation.PostConstruct; +import javax.ws.rs.Produces; +import javax.ws.rs.ext.ContextResolver; +import com.fasterxml.jackson.databind.ObjectMapper; import org.glassfish.jersey.server.ResourceConfig; import org.glassfish.jersey.server.model.Resource; @@ -32,6 +35,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; 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,6 +89,12 @@ class JerseyWebEndpointManagementContextConfiguration { || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT); } + @Bean + ResourceConfigCustomizer actuatorResourceConfigCustomizer(ActuatorJsonMapperProvider jsonMapperProvider) { + return (ResourceConfig config) -> config.register( + new ActuatorJsonMapperContextResolver(jsonMapperProvider.getInstance()), ContextResolver.class); + } + /** * Register endpoints with the {@link ResourceConfig}. The * {@link ResourceConfigCustomizer} cannot be used because we don't want to apply @@ -145,4 +156,20 @@ class JerseyWebEndpointManagementContextConfiguration { } + @Produces({ ActuatorMediaType.V3_JSON, ActuatorMediaType.V2_JSON }) + private static final class ActuatorJsonMapperContextResolver implements ContextResolver { + + private final ObjectMapper objectMapper; + + private ActuatorJsonMapperContextResolver(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public ObjectMapper getContext(Class type) { + return this.objectMapper; + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java index aa758ae72d7..2b7f7cd4fbf 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/reactive/WebFluxEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -26,6 +26,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -40,8 +42,13 @@ 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.context.properties.EnableConfigurationProperties; +import org.springframework.boot.web.codec.CodecCustomizer; import org.springframework.context.annotation.Bean; +import org.springframework.core.annotation.Order; import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.http.codec.CodecConfigurer; +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; @@ -52,6 +59,7 @@ import org.springframework.web.reactive.DispatcherHandler; * * @author Andy Wilkinson * @author Phillip Webb + * @author Brian Clozel * @since 2.0.0 */ @ManagementContextConfiguration(proxyBeanMethods = false) @@ -93,4 +101,16 @@ public class WebFluxEndpointManagementContextConfiguration { corsProperties.toCorsConfiguration()); } + @Bean + @Order(-1) + public CodecCustomizer actuatorJsonCodec(ActuatorJsonMapperProvider actuatorJsonMapperProvider) { + return (configurer) -> { + MediaType v3MediaType = MediaType.parseMediaType(ActuatorMediaType.V3_JSON); + MediaType v2MediaType = MediaType.parseMediaType(ActuatorMediaType.V2_JSON); + CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs(); + customCodecs.register( + new Jackson2JsonEncoder(actuatorJsonMapperProvider.getInstance(), v3MediaType, v2MediaType)); + }; + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java index 91064d4d5ae..86b426b63fa 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/servlet/WebMvcEndpointManagementContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -20,12 +20,16 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import com.fasterxml.jackson.databind.ObjectMapper; + import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType; import org.springframework.boot.actuate.endpoint.ExposableEndpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; @@ -42,9 +46,15 @@ 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.Configuration; +import org.springframework.core.Ordered; import org.springframework.core.env.Environment; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; 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 @@ -91,4 +101,35 @@ public class WebMvcEndpointManagementContextConfiguration { corsProperties.toCorsConfiguration()); } + @Configuration(proxyBeanMethods = false) + public static class JsonWebMvcConfigurer implements WebMvcConfigurer, Ordered { + + private final ActuatorJsonMapperProvider actuatorJsonMapperProvider; + + public JsonWebMvcConfigurer(ActuatorJsonMapperProvider objectMapperFactory) { + this.actuatorJsonMapperProvider = objectMapperFactory; + } + + @Override + public void configureMessageConverters(List> converters) { + converters.add(new ActuatorJsonHttpMessageConverter(this.actuatorJsonMapperProvider.getInstance())); + } + + // WebMvcAutoConfiguration is ordered at 0 + @Override + public int getOrder() { + return -1; + } + + } + + static class ActuatorJsonHttpMessageConverter extends AbstractJackson2HttpMessageConverter { + + ActuatorJsonHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, MediaType.parseMediaType(ActuatorMediaType.V3_JSON), + MediaType.parseMediaType(ActuatorMediaType.V2_JSON)); + } + + } + } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java index 953c292fdb6..24352170654 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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,9 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.web.servlet; +import java.util.List; + import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration; import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType; +import org.springframework.boot.actuate.endpoint.json.ActuatorJsonMapperProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -33,11 +37,15 @@ import org.springframework.boot.web.servlet.error.ErrorAttributes; import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter; import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; import org.springframework.web.context.request.RequestContextListener; import org.springframework.web.filter.RequestContextFilter; import org.springframework.web.servlet.DispatcherServlet; import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC @@ -108,6 +116,29 @@ class WebMvcEndpointChildContextConfiguration { return new OrderedRequestContextFilter(); } + /** + * Since {@code WebMvcEndpointManagementContextConfiguration} is adding an + * actuator-specific JSON message converter, {@code @EnableWebMvc} will not register + * default converters. We need to register a JSON converter for plain + * {@code "application/json"} still. + * WebMvcEndpointChildContextConfigurationIntegrationTests + */ + @Configuration(proxyBeanMethods = false) + public static class FallbackJsonConverterConfigurer implements WebMvcConfigurer { + + private final ActuatorJsonMapperProvider actuatorJsonMapperProvider; + + FallbackJsonConverterConfigurer(ObjectProvider objectMapperSupplier) { + this.actuatorJsonMapperProvider = objectMapperSupplier.getIfAvailable(ActuatorJsonMapperProvider::new); + } + + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MappingJackson2HttpMessageConverter(this.actuatorJsonMapperProvider.getInstance())); + } + + } + /** * {@link WebServerFactoryCustomizer} to add an {@link ErrorPage} so that the * {@link ManagementErrorEndpoint} can be used. diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java index 03d946837ae..30f06fabbe5 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/ScheduledTasksEndpointDocumentationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -52,7 +52,7 @@ class ScheduledTasksEndpointDocumentationTests extends MockMvcEndpointDocumentat @Test void scheduledTasks() throws Exception { - this.mockMvc.perform(get("/actuator/scheduledtasks")).andExpect(status().isOk()) + this.mockMvc.perform(get("/actuator/scheduledtasks").accept("application/json")).andExpect(status().isOk()) .andDo(document("scheduled-tasks", preprocessResponse(replacePattern( Pattern.compile("org.*\\.ScheduledTasksEndpointDocumentationTests\\$TestConfiguration"), diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java index 5e161080ed5..77b57684a6e 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/jersey/JerseyWebEndpointManagementContextConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -21,6 +21,7 @@ import java.util.Collections; import org.glassfish.jersey.server.ResourceConfig; import org.junit.jupiter.api.Test; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.endpoint.web.jersey.JerseyWebEndpointManagementContextConfiguration.JerseyWebEndpointsResourcesRegistrar; import org.springframework.boot.actuate.autoconfigure.web.jersey.JerseySameManagementContextConfiguration; @@ -41,8 +42,8 @@ import static org.assertj.core.api.Assertions.assertThat; class JerseyWebEndpointManagementContextConfigurationTests { private final WebApplicationContextRunner runner = new WebApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(WebEndpointAutoConfiguration.class, - JerseyWebEndpointManagementContextConfiguration.class)) + .withConfiguration(AutoConfigurations.of(EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, JerseyWebEndpointManagementContextConfiguration.class)) .withBean(WebEndpointsSupplier.class, () -> Collections::emptyList); @Test diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java index 36021b89f9b..8d33895bf04 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -20,7 +20,6 @@ import java.util.function.Supplier; import javax.servlet.http.HttpServlet; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration; @@ -33,32 +32,23 @@ import org.springframework.boot.actuate.endpoint.web.EndpointServlet; import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint; import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint; import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpoint; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; -import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration; -import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration; +import org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; -import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.http.MediaType; -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.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.hasKey; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.hamcrest.Matchers.startsWith; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @@ -67,98 +57,39 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * Integration tests for the Actuator's MVC endpoints. * * @author Andy Wilkinson + * @author Brian Clozel */ -class WebMvcEndpointIntegrationTests { +public class WebMvcEndpointIntegrationTests { - private AnnotationConfigServletWebApplicationContext context; - - @AfterEach - void close() { - TestSecurityContextHolder.clearContext(); - this.context.close(); - } - - @Test - void endpointsAreSecureByDefault() throws Exception { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.register(SecureConfiguration.class); - MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/actuator/beans").accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); - } - - @Test - void endpointsAreSecureByDefaultWithCustomBasePath() throws Exception { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.register(SecureConfiguration.class); - TestPropertyValues.of("management.endpoints.web.base-path:/management").applyTo(this.context); - MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/management/beans").accept(MediaType.APPLICATION_JSON)) - .andExpect(status().isUnauthorized()); - } - - @Test - void endpointsAreSecureWithActuatorRoleWithCustomBasePath() throws Exception { - TestSecurityContextHolder.getContext() - .setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.register(SecureConfiguration.class); - TestPropertyValues - .of("management.endpoints.web.base-path:/management", "management.endpoints.web.exposure.include=*") - .applyTo(this.context); - MockMvc mockMvc = createSecureMockMvc(); - mockMvc.perform(get("/management/beans")).andExpect(status().isOk()); - } + private WebApplicationContextRunner contextRunner = new WebApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class, GsonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class, + WebEndpointAutoConfiguration.class, ServletManagementContextAutoConfiguration.class, + AuditAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class, + WebMvcAutoConfiguration.class, ManagementContextAutoConfiguration.class, + AuditAutoConfiguration.class, DispatcherServletAutoConfiguration.class, + BeansEndpointAutoConfiguration.class)); @Test void linksAreProvidedToAllEndpointTypes() throws Exception { - this.context = new AnnotationConfigServletWebApplicationContext(); - this.context.register(DefaultConfiguration.class, EndpointsConfiguration.class); - TestPropertyValues.of("management.endpoints.web.exposure.include=*").applyTo(this.context); - MockMvc mockMvc = doCreateMockMvc(); - mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isOk()).andExpect(jsonPath("_links", - both(hasKey("beans")).and(hasKey("servlet")).and(hasKey("restcontroller")).and(hasKey("controller")))); + this.contextRunner.withUserConfiguration(EndpointsConfiguration.class) + .withPropertyValues("management.endpoints.web.exposure.include=*").run((context) -> { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + mockMvc.perform(get("/actuator").accept("*/*")).andExpect(status().isOk()) + .andExpect(jsonPath("_links", both(hasKey("beans")).and(hasKey("servlet")) + .and(hasKey("restcontroller")).and(hasKey("controller")))); + }); } - private MockMvc createSecureMockMvc() { - return doCreateMockMvc(springSecurity()); - } - - private MockMvc doCreateMockMvc(MockMvcConfigurer... configurers) { - this.context.setServletContext(new MockServletContext()); - this.context.refresh(); - DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.context); - for (MockMvcConfigurer configurer : configurers) { - builder.apply(configurer); - } - return builder.build(); - } - - @ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, - EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, - ServletManagementContextAutoConfiguration.class, AuditAutoConfiguration.class, - PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class, - ManagementContextAutoConfiguration.class, AuditAutoConfiguration.class, - DispatcherServletAutoConfiguration.class, BeansEndpointAutoConfiguration.class }) - static class DefaultConfiguration { - - } - - @Import(SecureConfiguration.class) - @ImportAutoConfiguration({ HypermediaAutoConfiguration.class }) - static class SpringHateoasConfiguration { - - } - - @Import(SecureConfiguration.class) - @ImportAutoConfiguration({ HypermediaAutoConfiguration.class, RepositoryRestMvcAutoConfiguration.class }) - static class SpringDataRestConfiguration { - - } - - @Import(DefaultConfiguration.class) - @ImportAutoConfiguration({ SecurityAutoConfiguration.class }) - static class SecureConfiguration { - + @Test + void dedicatedJsonMapperIsUsed() throws Exception { + this.contextRunner.withPropertyValues("spring.mvc.converters.preferred-json-mapper:gson", + "management.endpoints.web.exposure.include=*").run((context) -> { + MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); + mockMvc.perform(get("/actuator/beans").accept("*/*")).andExpect(status().isOk()) + .andExpect(MockMvcResultMatchers.header().string("Content-Type", + startsWith("application/vnd.spring-boot.actuator"))); + }); } @ServletEndpoint(id = "servlet") diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointSecurityIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointSecurityIntegrationTests.java new file mode 100644 index 00000000000..ffd80082769 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/integrationtest/WebMvcEndpointSecurityIntegrationTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2012-2020 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 org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration; +import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; +import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration; +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +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.setup.DefaultMockMvcBuilder; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.test.web.servlet.setup.MockMvcConfigurer; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Integration tests for the Actuator's MVC endpoints security features. + * + * @author Andy Wilkinson + */ +class WebMvcEndpointSecurityIntegrationTests { + + private AnnotationConfigServletWebApplicationContext context; + + @AfterEach + void close() { + TestSecurityContextHolder.clearContext(); + this.context.close(); + } + + @Test + void endpointsAreSecureByDefault() throws Exception { + this.context = new AnnotationConfigServletWebApplicationContext(); + this.context.register(SecureConfiguration.class); + MockMvc mockMvc = createSecureMockMvc(); + mockMvc.perform(get("/actuator/beans").accept(MediaType.APPLICATION_JSON)).andExpect(status().isUnauthorized()); + } + + @Test + void endpointsAreSecureByDefaultWithCustomBasePath() throws Exception { + this.context = new AnnotationConfigServletWebApplicationContext(); + this.context.register(SecureConfiguration.class); + TestPropertyValues.of("management.endpoints.web.base-path:/management").applyTo(this.context); + MockMvc mockMvc = createSecureMockMvc(); + mockMvc.perform(get("/management/beans").accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isUnauthorized()); + } + + @Test + void endpointsAreSecureWithActuatorRoleWithCustomBasePath() throws Exception { + TestSecurityContextHolder.getContext() + .setAuthentication(new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR")); + this.context = new AnnotationConfigServletWebApplicationContext(); + this.context.register(SecureConfiguration.class); + TestPropertyValues + .of("management.endpoints.web.base-path:/management", "management.endpoints.web.exposure.include=*") + .applyTo(this.context); + MockMvc mockMvc = createSecureMockMvc(); + mockMvc.perform(get("/management/beans")).andExpect(status().isOk()); + } + + private MockMvc createSecureMockMvc() { + return doCreateMockMvc(springSecurity()); + } + + private MockMvc doCreateMockMvc(MockMvcConfigurer... configurers) { + this.context.setServletContext(new MockServletContext()); + this.context.refresh(); + DefaultMockMvcBuilder builder = MockMvcBuilders.webAppContextSetup(this.context); + for (MockMvcConfigurer configurer : configurers) { + builder.apply(configurer); + } + return builder.build(); + } + + @ImportAutoConfiguration({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class, + EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class, + ServletManagementContextAutoConfiguration.class, AuditAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementContextAutoConfiguration.class, AuditAutoConfiguration.class, + DispatcherServletAutoConfiguration.class, BeansEndpointAutoConfiguration.class }) + static class DefaultConfiguration { + + } + + @Import(DefaultConfiguration.class) + @ImportAutoConfiguration({ SecurityAutoConfiguration.class }) + static class SecureConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java index d82ea973b7d..815cd37f500 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/web/servlet/WebMvcEndpointChildContextConfigurationIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2019 the original author or authors. + * Copyright 2012-2020 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. @@ -59,6 +59,7 @@ class WebMvcEndpointChildContextConfigurationIntegrationTests { WebClient client = WebClient.create("http://localhost:" + port); ClientResponse response = client.get().uri("actuator/fail").accept(MediaType.APPLICATION_JSON) .exchange().block(); + assertThat(response.headers().contentType().get()).isEqualTo(MediaType.APPLICATION_JSON); assertThat(response.bodyToMono(String.class).block()).contains("message\":\"Epic Fail"); }); } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/ActuatorJsonMapperProvider.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/ActuatorJsonMapperProvider.java new file mode 100644 index 00000000000..faf5ffd2cf4 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/ActuatorJsonMapperProvider.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2020 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.json; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; + +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +/** + * Factory for an {@code ObjectMapper} instance to be shared within Actuator + * infrastructure. + * + *

+ * The goal is to have a Jackson configuration separate from the rest of the application + * to keep a consistent serialization behavior between applications. + * + * @author Brian Clozel + * @since 2.3.0 + */ +public class ActuatorJsonMapperProvider { + + private ObjectMapper objectMapper; + + public ObjectMapper getInstance() { + if (this.objectMapper == null) { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder(); + builder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + builder.featuresToDisable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS); + this.objectMapper = builder.build(); + } + return this.objectMapper; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/package-info.java new file mode 100644 index 00000000000..ea8c5cce239 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/json/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2020 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. + */ + +/** + * JSON support for actuator endpoints. + */ +package org.springframework.boot.actuate.endpoint.json; diff --git a/src/checkstyle/import-control.xml b/src/checkstyle/import-control.xml index fb8206fe27c..bd7c2283872 100644 --- a/src/checkstyle/import-control.xml +++ b/src/checkstyle/import-control.xml @@ -46,6 +46,9 @@ + + +