From b900a3efc87d1d24f37bbf485df9611885233d9c Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Thu, 19 Jan 2017 15:25:48 +0000 Subject: [PATCH] Update Actuator endpoints to use custom media type Previously, the actuator's endpoints produced application/json and, where appropriate, also consumed application/json. Without a custom, versioned media type, it's impossible for us to make changes to the endpoints without breaking clients. This commit introduces a new media type, application/spring-boot.actuator.v1+json, that is now produced by default with application/json also being produced if requested. Endpoints that consume JSON will now also accept content the uses the new media type in addition to application/json. Closes gh-7967 --- .../hypermedia/EndpointDocumentation.java | 29 +++-- .../HealthEndpointDocumentation.java | 6 +- .../HypermediaEndpointDocumentation.java | 14 ++- ...ermediaManagementContextConfiguration.java | 30 ++++- .../endpoint/mvc/ActuatorGetMapping.java | 53 ++++++++ .../endpoint/mvc/ActuatorMediaTypes.java | 43 +++++++ .../endpoint/mvc/ActuatorPostMapping.java | 57 +++++++++ .../endpoint/mvc/AuditEventsMvcEndpoint.java | 6 +- .../endpoint/mvc/EndpointMvcAdapter.java | 6 +- .../endpoint/mvc/EnvironmentMvcEndpoint.java | 6 +- .../endpoint/mvc/HalJsonMvcEndpoint.java | 4 +- .../endpoint/mvc/HealthMvcEndpoint.java | 4 +- .../endpoint/mvc/LoggersMvcEndpoint.java | 7 +- .../endpoint/mvc/MetricsMvcEndpoint.java | 6 +- .../endpoint/mvc/ShutdownMvcEndpoint.java | 6 +- .../mvc/AuditEventsMvcEndpointTests.java | 20 ++- .../mvc/EnvironmentMvcEndpointTests.java | 33 +++++ .../endpoint/mvc/HealthMvcEndpointTests.java | 14 +-- .../endpoint/mvc/InfoMvcEndpointTests.java | 20 ++- .../endpoint/mvc/LoggersMvcEndpointTests.java | 27 +++- .../endpoint/mvc/MetricsMvcEndpointTests.java | 35 +++++- .../mvc/ShutdownMvcEndpointTests.java | 115 ++++++++++++++++-- ...ediaHttpMessageConverterConfiguration.java | 17 ++- 23 files changed, 478 insertions(+), 80 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorGetMapping.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMediaTypes.java create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorPostMapping.java diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java index 7694def6ba8..e2be9108359 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/EndpointDocumentation.java @@ -37,6 +37,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.MvcEndpoints; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; @@ -76,9 +77,9 @@ public class EndpointDocumentation { static final File LOG_FILE = new File("target/logs/spring.log"); - private static final Set SKIPPED = Collections.unmodifiableSet( - new HashSet(Arrays.asList("/docs", "/logfile", "/heapdump", - "/auditevents"))); + private static final Set SKIPPED = Collections + .unmodifiableSet(new HashSet( + Arrays.asList("/docs", "/logfile", "/heapdump", "/auditevents"))); @Autowired private MvcEndpoints mvcEndpoints; @@ -123,34 +124,36 @@ public class EndpointDocumentation { public void setLogger() throws Exception { this.mockMvc .perform(post("/loggers/org.springframework.boot") - .contentType(MediaType.APPLICATION_JSON) + .contentType(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON) .content("{\"configuredLevel\": \"DEBUG\"}")) .andExpect(status().isOk()).andDo(document("set-logger")); } @Test public void auditEvents() throws Exception { - this.mockMvc.perform(get("/auditevents") - .param("after", "2016-11-01T10:00:00+0000") - .accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get("/auditevents").param("after", "2016-11-01T10:00:00+0000") + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()).andDo(document("auditevents")); } @Test public void auditEventsByPrincipal() throws Exception { - this.mockMvc.perform(get("/auditevents").param("principal", "admin") + this.mockMvc + .perform(get("/auditevents").param("principal", "admin") .param("after", "2016-11-01T10:00:00+0000") - .accept(MediaType.APPLICATION_JSON)) + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()) .andDo(document("auditevents/filter-by-principal")); } @Test public void auditEventsByPrincipalAndType() throws Exception { - this.mockMvc.perform(get("/auditevents").param("principal", "admin") + this.mockMvc + .perform(get("/auditevents").param("principal", "admin") .param("after", "2016-11-01T10:00:00+0000") .param("type", "AUTHENTICATION_SUCCESS") - .accept(MediaType.APPLICATION_JSON)) + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()) .andDo(document("auditevents/filter-by-principal-and-type")); } @@ -167,7 +170,9 @@ public class EndpointDocumentation { if (!SKIPPED.contains(endpointPath)) { String output = endpointPath.substring(1); output = output.length() > 0 ? output : "./"; - this.mockMvc.perform(get(endpointPath).accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get(endpointPath) + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()).andDo(document(output)) .andDo(new ResultHandler() { diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java index d52386d5881..5562034eeb0 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HealthEndpointDocumentation.java @@ -20,10 +20,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootContextLoader; -import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -50,7 +50,9 @@ public class HealthEndpointDocumentation { @Test public void health() throws Exception { - this.mockMvc.perform(get("/health").accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get("/health") + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()).andDo(document("health/insensitive")); } diff --git a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java index a94ba09a0e8..efb216af335 100644 --- a/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java +++ b/spring-boot-actuator-docs/src/restdoc/java/org/springframework/boot/actuate/hypermedia/HypermediaEndpointDocumentation.java @@ -20,10 +20,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootContextLoader; -import org.springframework.http.MediaType; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestPropertySource; @@ -51,13 +51,17 @@ public class HypermediaEndpointDocumentation { @Test public void beans() throws Exception { - this.mockMvc.perform(get("/beans").accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get("/beans") + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()).andDo(document("beans/hypermedia")); } @Test public void metrics() throws Exception { - this.mockMvc.perform(get("/metrics").accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get("/metrics") + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()) .andExpect(jsonPath("$._links.self.href") .value("http://localhost:8080/metrics")) @@ -66,7 +70,9 @@ public class HypermediaEndpointDocumentation { @Test public void home() throws Exception { - this.mockMvc.perform(get("/actuator").accept(MediaType.APPLICATION_JSON)) + this.mockMvc + .perform(get("/actuator") + .accept(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON)) .andExpect(status().isOk()).andDo(document("admin")); } diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java index 28ddfafa0ba..a1a831efc61 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/EndpointWebMvcHypermediaManagementContextConfiguration.java @@ -18,6 +18,7 @@ package org.springframework.boot.actuate.autoconfigure; import java.io.IOException; import java.lang.reflect.Type; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.actuate.autoconfigure.EndpointWebMvcHypermediaManagementContextConfiguration.EndpointHypermediaEnabledCondition; import org.springframework.boot.actuate.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.endpoint.mvc.ActuatorMediaTypes; import org.springframework.boot.actuate.endpoint.mvc.DocsMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HalBrowserMvcEndpoint; import org.springframework.boot.actuate.endpoint.mvc.HalJsonMvcEndpoint; @@ -64,7 +66,9 @@ import org.springframework.hateoas.ResourceSupport; import org.springframework.hateoas.UriTemplate; import org.springframework.hateoas.hal.CurieProvider; import org.springframework.hateoas.hal.DefaultCurieProvider; +import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageNotWritableException; import org.springframework.http.server.ServerHttpRequest; @@ -221,13 +225,33 @@ public class EndpointWebMvcHypermediaManagementContextConfiguration { */ @ConditionalOnProperty(prefix = "endpoints.hypermedia", name = "enabled", matchIfMissing = false) @ControllerAdvice(assignableTypes = MvcEndpoint.class) - public static class MvcEndpointAdvice implements ResponseBodyAdvice { + static class MvcEndpointAdvice implements ResponseBodyAdvice { - @Autowired - private List handlerAdapters; + private final List handlerAdapters; private final Map> converterCache = new ConcurrentHashMap>(); + MvcEndpointAdvice(List handlerAdapters) { + this.handlerAdapters = handlerAdapters; + } + + @PostConstruct + public void configureHttpMessageConverters() { + for (RequestMappingHandlerAdapter handlerAdapter : this.handlerAdapters) { + for (HttpMessageConverter messageConverter : handlerAdapter + .getMessageConverters()) { + if (messageConverter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { + List supportedMediaTypes = new ArrayList( + messageConverter.getSupportedMediaTypes()); + supportedMediaTypes + .add(ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON); + ((AbstractHttpMessageConverter) messageConverter) + .setSupportedMediaTypes(supportedMediaTypes); + } + } + } + } + @Override public boolean supports(MethodParameter returnType, Class> converterType) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorGetMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorGetMapping.java new file mode 100644 index 00000000000..1b9109214ed --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorGetMapping.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.mvc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Specialized {@link RequestMapping} for {@link RequestMethod#GET GET} requests that + * produce {@code application/json} or + * {@code application/vnd.spring-boot.actuator.v1+json} responses. + * + * @author Andy Wilkinson + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.GET, produces = { + ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) +@interface ActuatorGetMapping { + + /** + * Alias for {@link RequestMapping#value}. + * + * @return the value + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMediaTypes.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMediaTypes.java new file mode 100644 index 00000000000..9e43c5b9b89 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorMediaTypes.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.mvc; + +import org.springframework.http.MediaType; + +/** + * {@link MediaType MediaTypes} that can be consumed and produced by Actuator endpoints. + * + * @author Andy Wilkinson + */ +public final class ActuatorMediaTypes { + + /** + * {@link String} equivalent of {@link #APPLICATION_ACTUATOR_V1_JSON}. + */ + public static final String APPLICATION_ACTUATOR_V1_JSON_VALUE = "application/vnd.spring-boot.actuator.v1+json"; + + /** + * The {@code application/vnd.spring-boot.actuator.v1+json} media type. + */ + public static final MediaType APPLICATION_ACTUATOR_V1_JSON = MediaType + .valueOf(APPLICATION_ACTUATOR_V1_JSON_VALUE); + + private ActuatorMediaTypes() { + + } + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorPostMapping.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorPostMapping.java new file mode 100644 index 00000000000..d45f33d1a5a --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/ActuatorPostMapping.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2017 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 + * + * http://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.mvc; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; + +/** + * Specialized {@link RequestMapping} for {@link RequestMethod#POST POST} requests that + * consume {@code application/json} or + * {@code application/vnd.spring-boot.actuator.v1+json} requests and produce + * {@code application/json} or {@code application/vnd.spring-boot.actuator.v1+json} + * responses. + * + * @author Andy Wilkinson + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@RequestMapping(method = RequestMethod.POST, consumes = { + ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }, produces = { + ActuatorMediaTypes.APPLICATION_ACTUATOR_V1_JSON_VALUE, + MediaType.APPLICATION_JSON_VALUE }) +@interface ActuatorPostMapping { + + /** + * Alias for {@link RequestMapping#value}. + * + * @return the value + */ + @AliasFor(annotation = RequestMapping.class) + String[] value() default {}; + +} diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java index cffd0e85161..a30b32990bd 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/AuditEventsMvcEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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. @@ -24,10 +24,8 @@ import org.springframework.boot.actuate.audit.AuditEvent; import org.springframework.boot.actuate.audit.AuditEventRepository; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.format.annotation.DateTimeFormat; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @@ -49,7 +47,7 @@ public class AuditEventsMvcEndpoint extends AbstractNamedMvcEndpoint { this.auditEventRepository = auditEventRepository; } - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ActuatorGetMapping @ResponseBody public ResponseEntity findByPrincipalAndAfterAndType( @RequestParam(required = false) String principal, diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java index db841a09f16..1cd2e1ad301 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EndpointMvcAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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,8 +17,6 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.springframework.boot.actuate.endpoint.Endpoint; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.ResponseBody; /** @@ -38,7 +36,7 @@ public class EndpointMvcAdapter extends AbstractEndpointMvcAdapter> } @Override - @GetMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ActuatorGetMapping @ResponseBody public Object invoke() { return super.invoke(); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java index 840232405af..cd44c35eb76 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/EnvironmentMvcEndpoint.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2016 the original author or authors. + * Copyright 2012-2017 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,8 +25,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @@ -48,7 +46,7 @@ public class EnvironmentMvcEndpoint extends EndpointMvcAdapter super(delegate); } - @GetMapping(value = "/{name:.*}", produces = MediaType.APPLICATION_JSON_VALUE) + @ActuatorGetMapping("/{name:.*}") @ResponseBody @HypermediaDisabled public Object value(@PathVariable String name) { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java index 31c680ebf45..0c4fd449114 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HalJsonMvcEndpoint.java @@ -18,9 +18,7 @@ package org.springframework.boot.actuate.endpoint.mvc; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.hateoas.ResourceSupport; -import org.springframework.http.MediaType; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** @@ -49,7 +47,7 @@ public class HalJsonMvcEndpoint extends AbstractNamedMvcEndpoint { return "/actuator"; } - @RequestMapping(produces = MediaType.APPLICATION_JSON_VALUE) + @ActuatorGetMapping @ResponseBody public ResourceSupport links() { return new ResourceSupport(); diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java index bd71e0dc930..9c15b55117b 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/mvc/HealthMvcEndpoint.java @@ -30,11 +30,9 @@ import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.EnvironmentAware; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; /** @@ -123,7 +121,7 @@ public class HealthMvcEndpoint extends AbstractEndpointMvcAdapter> response = (ResponseEntity>) this.mvc - .invoke(); - assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND); + public void contentTypeDefaultsToActuatorV1Json() throws Exception { + this.mvc.perform(post("/shutdown")).andExpect(status().isOk()) + .andExpect(header().string("Content-Type", + "application/vnd.spring-boot.actuator.v1+json;charset=UTF-8")); + assertThat(this.context.getBean(CountDownLatch.class).await(30, TimeUnit.SECONDS)) + .isTrue(); + } + + @Test + public void contentTypeCanBeApplicationJson() throws Exception { + this.mvc.perform(post("/shutdown").header(HttpHeaders.ACCEPT, + MediaType.APPLICATION_JSON_VALUE)).andExpect(status().isOk()) + .andExpect(header().string("Content-Type", + MediaType.APPLICATION_JSON_UTF8_VALUE)); + assertThat(this.context.getBean(CountDownLatch.class).await(30, TimeUnit.SECONDS)) + .isTrue(); + } + + @Configuration + @Import({ JacksonAutoConfiguration.class, + HttpMessageConvertersAutoConfiguration.class, + EndpointWebMvcAutoConfiguration.class, WebMvcAutoConfiguration.class, + ManagementServerPropertiesAutoConfiguration.class }) + public static class TestConfiguration { + + @Bean + public TestShutdownEndpoint endpoint() { + return new TestShutdownEndpoint(contextCloseLatch()); + } + + @Bean + public CountDownLatch contextCloseLatch() { + return new CountDownLatch(1); + } + + } + + private static class TestShutdownEndpoint extends ShutdownEndpoint { + + private final CountDownLatch contextCloseLatch; + + TestShutdownEndpoint(CountDownLatch contextCloseLatch) { + this.contextCloseLatch = contextCloseLatch; + } + + @Override + public void setApplicationContext(ApplicationContext context) + throws BeansException { + ConfigurableApplicationContext mockContext = mock( + ConfigurableApplicationContext.class); + willAnswer(new Answer() { + + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + TestShutdownEndpoint.this.contextCloseLatch.countDown(); + return null; + } + + }).given(mockContext).close(); + super.setApplicationContext(mockContext); + } + } } diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java index c7f0c4121cc..42c3790a29b 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaHttpMessageConverterConfiguration.java @@ -16,7 +16,8 @@ package org.springframework.boot.autoconfigure.hateoas; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -29,9 +30,9 @@ import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; -import org.springframework.hateoas.MediaTypes; import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter; import org.springframework.http.MediaType; +import org.springframework.http.converter.AbstractHttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; @@ -72,10 +73,14 @@ public class HypermediaHttpMessageConverterConfiguration { for (HttpMessageConverter converter : handlerAdapter .getMessageConverters()) { if (converter instanceof TypeConstrainedMappingJackson2HttpMessageConverter) { - ((TypeConstrainedMappingJackson2HttpMessageConverter) converter) - .setSupportedMediaTypes( - Arrays.asList(MediaTypes.HAL_JSON, - MediaType.APPLICATION_JSON)); + List supportedMediaTypes = new ArrayList( + converter.getSupportedMediaTypes()); + if (!supportedMediaTypes + .contains(MediaType.APPLICATION_JSON)) { + supportedMediaTypes.add(MediaType.APPLICATION_JSON); + } + ((AbstractHttpMessageConverter) converter) + .setSupportedMediaTypes(supportedMediaTypes); } }