Add direct WebFlux and WebMvc endpoint support

Add `@ControllerEndpoint` and `@RestControllerEndpoint` annotations that
can be used to develop a Spring-only request mapped endpoint. Both
Spring MVC and Spring WebFlux are supported.

This feature is primarily for use when deeper Spring integration is
required or when existing Spring Boot 1.5 projects want to migrate to
Spring Boot 2.0 without re-writing existing endpoints. It comes at the
expense of portability, since such endpoints will be missing from
Jersey.

Fixes gh-10257
pull/11626/merge
Phillip Webb 7 years ago
parent 340ef52f78
commit bda9b892b3

@ -36,6 +36,9 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints; import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoints;
import org.springframework.boot.actuate.endpoint.web.PathMapper; import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer; import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@ -100,6 +103,16 @@ public class WebEndpointAutoConfiguration {
filters.getIfAvailable(Collections::emptyList)); filters.getIfAvailable(Collections::emptyList));
} }
@Bean
@ConditionalOnMissingBean(ControllerEndpointsSupplier.class)
public ControllerEndpointDiscoverer controllerEndpointDiscoverer(
PathMapper webEndpointPathMapper,
ObjectProvider<Collection<OperationInvokerAdvisor>> invokerAdvisors,
ObjectProvider<Collection<EndpointFilter<ExposableControllerEndpoint>>> filters) {
return new ControllerEndpointDiscoverer(this.applicationContext,
webEndpointPathMapper, filters.getIfAvailable(Collections::emptyList));
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public PathMappedEndpoints pathMappedEndpoints( public PathMappedEndpoints pathMappedEndpoints(
@ -117,4 +130,12 @@ public class WebEndpointAutoConfiguration {
expose, exclude, "info", "health"); expose, exclude, "info", "health");
} }
@Bean
public ExposeExcludePropertyEndpointFilter<ExposableControllerEndpoint> controllerIncludeExcludePropertyEndpointFilter() {
Set<String> expose = this.properties.getExpose();
Set<String> exclude = this.properties.getExclude();
return new ExposeExcludePropertyEndpointFilter<>(
ExposableControllerEndpoint.class, expose, exclude);
}
} }

@ -22,6 +22,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.reactive.ControllerEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -61,4 +63,17 @@ public class WebFluxEndpointManagementContextConfiguration {
corsProperties.toCorsConfiguration()); corsProperties.toCorsConfiguration());
} }
@Bean
@ConditionalOnMissingBean
public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) {
EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath());
return new ControllerEndpointHandlerMapping(endpointMapping,
controllerEndpointsSupplier.getEndpoints(),
corsProperties.toCorsConfiguration());
}
} }

@ -22,6 +22,8 @@ import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfi
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier; import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.servlet.ControllerEndpointHandlerMapping;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping; import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -60,4 +62,17 @@ public class WebMvcEndpointManagementContextConfiguration {
corsProperties.toCorsConfiguration()); corsProperties.toCorsConfiguration());
} }
@Bean
@ConditionalOnMissingBean
public ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
ControllerEndpointsSupplier controllerEndpointsSupplier,
CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties) {
EndpointMapping endpointMapping = new EndpointMapping(
webEndpointProperties.getBasePath());
return new ControllerEndpointHandlerMapping(endpointMapping,
controllerEndpointsSupplier.getEndpoints(),
corsProperties.toCorsConfiguration());
}
} }

@ -0,0 +1,94 @@
/*
* Copyright 2012-2018 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.autoconfigure.integrationtest;
import org.junit.After;
import org.junit.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.reactive.ReactiveManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
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.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebApplicationContext;
import org.springframework.security.authentication.TestingAuthenticationToken;
import org.springframework.security.test.context.TestSecurityContextHolder;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Integration tests for the Actuator's WebFlux {@link ControllerEndpoint controller
* endpoints}.
*
* @author Phillip Webb
*/
public class ControllerEndpointWebFluxIntegrationTests {
private AnnotationConfigReactiveWebApplicationContext context;
@After
public void close() {
TestSecurityContextHolder.clearContext();
this.context.close();
}
@Test
public void endpointsCanBeAccessed() throws Exception {
TestSecurityContextHolder.getContext().setAuthentication(
new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR"));
this.context = new AnnotationConfigReactiveWebApplicationContext();
this.context.register(DefaultConfiguration.class, ExampleController.class);
TestPropertyValues.of("management.endpoints.web.expose=*").applyTo(this.context);
this.context.refresh();
WebTestClient webClient = WebTestClient.bindToApplicationContext(this.context)
.build();
webClient.get().uri("/actuator/example").exchange().expectStatus().isOk();
}
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, EndpointAutoConfiguration.class,
WebEndpointAutoConfiguration.class,
ReactiveManagementContextAutoConfiguration.class,
AuditAutoConfiguration.class, PropertyPlaceholderAutoConfiguration.class,
WebFluxAutoConfiguration.class, ManagementContextAutoConfiguration.class,
AuditAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
BeansEndpointAutoConfiguration.class })
static class DefaultConfiguration {
}
@RestControllerEndpoint(id = "example")
static class ExampleController {
@GetMapping("/")
public String example() {
return "Example";
}
}
}

@ -0,0 +1,134 @@
/*
* Copyright 2012-2018 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.autoconfigure.integrationtest;
import org.junit.After;
import org.junit.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.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
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.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 org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
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 {@link ControllerEndpoint controller
* endpoints}.
*
* @author Phillip Webb
* @author Andy Wilkinson
*/
public class ControllerEndpointWebMvcIntegrationTests {
private AnnotationConfigWebApplicationContext context;
@After
public void close() {
TestSecurityContextHolder.clearContext();
this.context.close();
}
@Test
public void endpointsAreSecureByDefault() throws Exception {
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class, ExampleController.class);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/actuator/example").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isUnauthorized());
}
@Test
public void endpointsCanBeAccessed() throws Exception {
TestSecurityContextHolder.getContext().setAuthentication(
new TestingAuthenticationToken("user", "N/A", "ROLE_ACTUATOR"));
this.context = new AnnotationConfigWebApplicationContext();
this.context.register(SecureConfiguration.class, ExampleController.class);
TestPropertyValues.of("management.endpoints.web.base-path:/management",
"management.endpoints.web.expose=*").applyTo(this.context);
MockMvc mockMvc = createSecureMockMvc();
mockMvc.perform(get("/management/example")).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 {
}
@RestControllerEndpoint(id = "example")
static class ExampleController {
@GetMapping("/")
public String example() {
return "Example";
}
}
}

@ -22,6 +22,7 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfi
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration; 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.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration; import org.springframework.boot.actuate.autoconfigure.web.servlet.ServletManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration; import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
@ -34,6 +35,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.bind.annotation.GetMapping;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request;
@ -59,7 +61,8 @@ public class WebMvcEndpointExposureIntegrationTests {
ManagementContextAutoConfiguration.class, ManagementContextAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class)) ServletManagementContextAutoConfiguration.class))
.withConfiguration( .withConfiguration(
AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL)); AutoConfigurations.of(EndpointAutoConfigurationClasses.ALL))
.withUserConfiguration(CustomMvcEndpoint.class);
@Test @Test
public void webEndpointsAreDisabledByDefault() { public void webEndpointsAreDisabledByDefault() {
@ -68,6 +71,7 @@ public class WebMvcEndpointExposureIntegrationTests {
assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "custommvc")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "env")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "env")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue();
@ -87,6 +91,7 @@ public class WebMvcEndpointExposureIntegrationTests {
assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "custommvc")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "env")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "env")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue();
@ -106,6 +111,7 @@ public class WebMvcEndpointExposureIntegrationTests {
assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "custommvc")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "env")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "env")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "health")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "health")).isFalse();
assertThat(isExposed(mvc, HttpMethod.GET, "info")).isFalse(); assertThat(isExposed(mvc, HttpMethod.GET, "info")).isFalse();
@ -126,6 +132,7 @@ public class WebMvcEndpointExposureIntegrationTests {
assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "beans")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "conditions")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "configprops")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "custommvc")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "env")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "env")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "health")).isTrue();
assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue(); assertThat(isExposed(mvc, HttpMethod.GET, "info")).isTrue();
@ -151,4 +158,14 @@ public class WebMvcEndpointExposureIntegrationTests {
.format("Unexpected %s HTTP status for " + "endpoint %s", status, path)); .format("Unexpected %s HTTP status for " + "endpoint %s", status, path));
} }
@RestControllerEndpoint(id = "custommvc")
static class CustomMvcEndpoint {
@GetMapping("/")
public String main() {
return "test";
}
}
} }

@ -37,19 +37,29 @@ public abstract class AbstractDiscoveredEndpoint<O extends Operation>
private final EndpointDiscoverer<?, ?> discoverer; private final EndpointDiscoverer<?, ?> discoverer;
private final Object endpointBean;
/** /**
* Create a mew {@link AbstractDiscoveredEndpoint} instance. * Create a mew {@link AbstractDiscoveredEndpoint} instance.
* @param discoverer the discoverer that discovered the endpoint * @param discoverer the discoverer that discovered the endpoint
* @param endpointBean the primary source bean
* @param id the ID of the endpoint * @param id the ID of the endpoint
* @param enabledByDefault if the endpoint is enabled by default * @param enabledByDefault if the endpoint is enabled by default
* @param operations the endpoint operations * @param operations the endpoint operations
*/ */
public AbstractDiscoveredEndpoint(EndpointDiscoverer<?, ?> discoverer, String id, public AbstractDiscoveredEndpoint(EndpointDiscoverer<?, ?> discoverer,
boolean enabledByDefault, Collection<? extends O> operations) { Object endpointBean, String id, boolean enabledByDefault,
Collection<? extends O> operations) {
super(id, enabledByDefault, operations); super(id, enabledByDefault, operations);
Assert.notNull(discoverer, "Discoverer must not be null"); Assert.notNull(discoverer, "Discoverer must not be null");
Assert.notNull(discoverer, "EndpointBean must not be null"); Assert.notNull(discoverer, "EndpointBean must not be null");
this.discoverer = discoverer; this.discoverer = discoverer;
this.endpointBean = endpointBean;
}
@Override
public Object getEndpointBean() {
return this.endpointBean;
} }
@Override @Override
@ -59,8 +69,9 @@ public abstract class AbstractDiscoveredEndpoint<O extends Operation>
@Override @Override
public String toString() { public String toString() {
ToStringCreator creator = new ToStringCreator(this).append("discoverer", ToStringCreator creator = new ToStringCreator(this)
this.discoverer.getClass().getName()); .append("discoverer", this.discoverer.getClass().getName())
.append("endpointBean", this.endpointBean.getClass().getName());
appendFields(creator); appendFields(creator);
return creator.toString(); return creator.toString();
} }

@ -35,4 +35,10 @@ public interface DiscoveredEndpoint<O extends Operation> extends ExposableEndpoi
*/ */
boolean wasDiscoveredBy(Class<? extends EndpointDiscoverer<?, ?>> discoverer); boolean wasDiscoveredBy(Class<? extends EndpointDiscoverer<?, ?>> discoverer);
/**
* Return the source bean that was used to construct the {@link DiscoveredEndpoint}.
* @return the source endpoint bean
*/
Object getEndpointBean();
} }

@ -209,7 +209,8 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
List<O> operations = indexed.values().stream().map(this::getLast) List<O> operations = indexed.values().stream().map(this::getLast)
.filter(Objects::nonNull).collect(Collectors.collectingAndThen( .filter(Objects::nonNull).collect(Collectors.collectingAndThen(
Collectors.toList(), Collections::unmodifiableList)); Collectors.toList(), Collections::unmodifiableList));
return createEndpoint(id, endpointBean.isEnabledByDefault(), operations); return createEndpoint(endpointBean.getBean(), id,
endpointBean.isEnabledByDefault(), operations);
} }
private void addOperations(MultiValueMap<OperationKey, O> indexed, String id, private void addOperations(MultiValueMap<OperationKey, O> indexed, String id,
@ -265,7 +266,18 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private boolean isEndpointExposed(EndpointBean endpointBean) { private boolean isEndpointExposed(EndpointBean endpointBean) {
return isFilterMatch(endpointBean.getFilter(), endpointBean) return isFilterMatch(endpointBean.getFilter(), endpointBean)
&& !isEndpointFiltered(endpointBean); && !isEndpointFiltered(endpointBean)
&& isEndpointExposed(endpointBean.getBean());
}
/**
* Determine if an endpoint bean should be exposed. Subclasses can override this
* method to provide additional logic.
* @param extensionBean the extension bean
* @return {@code true} if the extension is exposed
*/
protected boolean isEndpointExposed(Object extensionBean) {
return true;
} }
private boolean isEndpointFiltered(EndpointBean endpointBean) { private boolean isEndpointFiltered(EndpointBean endpointBean) {
@ -320,7 +332,7 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
private E getFilterEndpoint(EndpointBean endpointBean) { private E getFilterEndpoint(EndpointBean endpointBean) {
E endpoint = this.filterEndpoints.get(endpointBean); E endpoint = this.filterEndpoints.get(endpointBean);
if (endpoint == null) { if (endpoint == null) {
endpoint = createEndpoint(endpointBean.getId(), endpoint = createEndpoint(endpointBean.getBean(), endpointBean.getId(),
endpointBean.isEnabledByDefault(), Collections.emptySet()); endpointBean.isEnabledByDefault(), Collections.emptySet());
this.filterEndpoints.put(endpointBean, endpoint); this.filterEndpoints.put(endpointBean, endpoint);
} }
@ -335,13 +347,14 @@ public abstract class EndpointDiscoverer<E extends ExposableEndpoint<O>, O exten
/** /**
* Factory method called to create the {@link ExposableEndpoint endpoint}. * Factory method called to create the {@link ExposableEndpoint endpoint}.
* @param endpointBean the source endpoint bean
* @param id the ID of the endpoint * @param id the ID of the endpoint
* @param enabledByDefault if the endpoint is enabled by default * @param enabledByDefault if the endpoint is enabled by default
* @param operations the endpoint operations * @param operations the endpoint operations
* @return a created endpoint (a {@link DiscoveredEndpoint} is recommended) * @return a created endpoint (a {@link DiscoveredEndpoint} is recommended)
*/ */
protected abstract E createEndpoint(String id, boolean enabledByDefault, protected abstract E createEndpoint(Object endpointBean, String id,
Collection<O> operations); boolean enabledByDefault, Collection<O> operations);
/** /**
* Factory method to create an {@link Operation endpoint operation}. * Factory method to create an {@link Operation endpoint operation}.

@ -31,9 +31,9 @@ import org.springframework.boot.actuate.endpoint.jmx.JmxOperation;
class DiscoveredJmxEndpoint extends AbstractDiscoveredEndpoint<JmxOperation> class DiscoveredJmxEndpoint extends AbstractDiscoveredEndpoint<JmxOperation>
implements ExposableJmxEndpoint { implements ExposableJmxEndpoint {
DiscoveredJmxEndpoint(EndpointDiscoverer<?, ?> discoverer, String id, DiscoveredJmxEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean,
boolean enabledByDefault, Collection<JmxOperation> operations) { String id, boolean enabledByDefault, Collection<JmxOperation> operations) {
super(discoverer, id, enabledByDefault, operations); super(discoverer, endpointBean, id, enabledByDefault, operations);
} }
} }

@ -54,9 +54,10 @@ public class JmxEndpointDiscoverer
} }
@Override @Override
protected ExposableJmxEndpoint createEndpoint(String id, boolean enabledByDefault, protected ExposableJmxEndpoint createEndpoint(Object endpointBean, String id,
Collection<JmxOperation> operations) { boolean enabledByDefault, Collection<JmxOperation> operations) {
return new DiscoveredJmxEndpoint(this, id, enabledByDefault, operations); return new DiscoveredJmxEndpoint(this, endpointBean, id, enabledByDefault,
operations);
} }
@Override @Override

@ -0,0 +1,72 @@
/*
* Copyright 2012-2018 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.web.annotation;
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.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
/**
* Identifies a type as being a rest endpoint that is only exposed over Spring MVC or
* Spring WebFlux. Mapped methods must be annotated with {@link GetMapping @GetMapping},
* {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc annotations
* rather than {@link ReadOperation @ReadOperation},
* {@link WriteOperation @WriteOperation}, {@link DeleteOperation @DeleteOperation}.
* <p>
* This annotation can be used when deeper Spring integration is required, but at the
* expense of portability. Most users should prefer the {@link Endpoint @Endpoint} or
* {@link WebEndpoint @WebEndpoint} annotation whenever possible.
*
* @author Phillip Webb
* @since 2.0.0
* @see WebEndpoint
* @see ControllerEndpoint
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Endpoint
@FilteredEndpoint(ControllerEndpointFilter.class)
public @interface ControllerEndpoint {
/**
* The id of the endpoint.
* @return the id
*/
@AliasFor(annotation = Endpoint.class)
String id();
/**
* If the endpoint should be enabled or disabled by default.
* @return {@code true} if the endpoint is enabled by default
*/
@AliasFor(annotation = Endpoint.class)
boolean enableByDefault() default true;
}

@ -0,0 +1,104 @@
/*
* Copyright 2012-2018 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.web.annotation;
import java.util.Collection;
import java.util.Collections;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.annotation.DiscoveredOperationMethod;
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
import org.springframework.boot.actuate.endpoint.invoke.ParameterMappingException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.util.Assert;
/**
* {@link EndpointDiscoverer} for {@link ExposableEndpoint controller endpoints}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public class ControllerEndpointDiscoverer
extends EndpointDiscoverer<ExposableControllerEndpoint, Operation>
implements ControllerEndpointsSupplier {
private final PathMapper endpointPathMapper;
/**
* Create a new {@link ControllerEndpointDiscoverer} instance.
* @param applicationContext the source application context
* @param endpointPathMapper the endpoint path mapper
* @param filters filters to apply
*/
public ControllerEndpointDiscoverer(ApplicationContext applicationContext,
PathMapper endpointPathMapper,
Collection<EndpointFilter<ExposableControllerEndpoint>> filters) {
super(applicationContext, new NoOpParameterValueMapper(), Collections.emptyList(),
filters);
Assert.notNull(endpointPathMapper, "EndpointPathMapper must not be null");
this.endpointPathMapper = endpointPathMapper;
}
@Override
protected boolean isEndpointExposed(Object endpointBean) {
Class<?> type = endpointBean.getClass();
return AnnotatedElementUtils.isAnnotated(type, ControllerEndpoint.class)
|| AnnotatedElementUtils.isAnnotated(type, RestControllerEndpoint.class);
}
@Override
protected ExposableControllerEndpoint createEndpoint(Object endpointBean, String id,
boolean enabledByDefault, Collection<Operation> operations) {
String rootPath = this.endpointPathMapper.getRootPath(id);
return new DiscoveredControllerEndpoint(this, endpointBean, id, rootPath,
enabledByDefault);
}
@Override
protected Operation createOperation(String endpointId,
DiscoveredOperationMethod operationMethod, OperationInvoker invoker) {
throw new IllegalStateException(
"ControllerEndpoints must not declare operations");
}
@Override
protected OperationKey createOperationKey(Operation operation) {
throw new IllegalStateException(
"ControllerEndpoints must not declare operations");
}
/**
* {@link ParameterValueMapper} that does nothing.
*/
private static class NoOpParameterValueMapper implements ParameterValueMapper {
@Override
public Object mapParameterValue(OperationParameter parameter, Object value)
throws ParameterMappingException {
return value;
}
}
}

@ -0,0 +1,34 @@
/*
* Copyright 2012-2018 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.web.annotation;
import org.springframework.boot.actuate.endpoint.EndpointFilter;
import org.springframework.boot.actuate.endpoint.annotation.DiscovererEndpointFilter;
/**
* {@link EndpointFilter} for endpoints discovered by
* {@link ControllerEndpointDiscoverer}.
*
* @author Phillip Webb
*/
class ControllerEndpointFilter extends DiscovererEndpointFilter {
ControllerEndpointFilter() {
super(ControllerEndpointDiscoverer.class);
}
}

@ -0,0 +1,30 @@
/*
* Copyright 2012-2018 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.web.annotation;
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
/**
* {@link EndpointsSupplier} for {@link ExposableControllerEndpoint controller endpoints}.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface ControllerEndpointsSupplier
extends EndpointsSupplier<ExposableControllerEndpoint> {
}

@ -0,0 +1,52 @@
/*
* Copyright 2012-2018 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.web.annotation;
import java.util.Collections;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.annotation.AbstractDiscoveredEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.EndpointDiscoverer;
/**
* A discovered {@link ExposableEndpoint controller endpoint}.
*
* @author Phillip Webb
*/
class DiscoveredControllerEndpoint extends AbstractDiscoveredEndpoint<Operation>
implements ExposableControllerEndpoint {
private final String rootPath;
DiscoveredControllerEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean,
String id, String rootPath, boolean enabledByDefault) {
super(discoverer, endpointBean, id, enabledByDefault, Collections.emptyList());
this.rootPath = rootPath;
}
@Override
public Object getController() {
return getEndpointBean();
}
@Override
public String getRootPath() {
return this.rootPath;
}
}

@ -33,9 +33,10 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint<WebOperation>
private final String rootPath; private final String rootPath;
DiscoveredWebEndpoint(EndpointDiscoverer<?, ?> discoverer, String id, String rootPath, DiscoveredWebEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean,
boolean enabledByDefault, Collection<WebOperation> operations) { String id, String rootPath, boolean enabledByDefault,
super(discoverer, id, enabledByDefault, operations); Collection<WebOperation> operations) {
super(discoverer, endpointBean, id, enabledByDefault, operations);
this.rootPath = rootPath; this.rootPath = rootPath;
} }
@ -43,5 +44,4 @@ class DiscoveredWebEndpoint extends AbstractDiscoveredEndpoint<WebOperation>
public String getRootPath() { public String getRootPath() {
return this.rootPath; return this.rootPath;
} }
} }

@ -0,0 +1,41 @@
/*
* Copyright 2012-2018 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.web.annotation;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.Operation;
import org.springframework.boot.actuate.endpoint.web.PathMappedEndpoint;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Information describing an endpoint that can be exposed over Spring MVC or Spring
* WebFlux. Mappings should be discovered directly from {@link #getController()} and
* {@link #getOperations() operation} should always return an empty collection.
*
* @author Phillip Webb
* @since 2.0.0
*/
public interface ExposableControllerEndpoint
extends ExposableEndpoint<Operation>, PathMappedEndpoint {
/**
* Return the source controller that contains {@link RequestMapping} methods.
* @return the source controller
*/
Object getController();
}

@ -0,0 +1,74 @@
/*
* Copyright 2012-2018 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.web.annotation;
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.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.FilteredEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.annotation.WriteOperation;
import org.springframework.core.annotation.AliasFor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* Identifies a type as being an rest endpoint that is only exposed over Spring MVC or
* Spring WebFlux. Mapped methods must be annotated with {@link GetMapping @GetMapping},
* {@link PostMapping @PostMapping}, {@link DeleteMapping @DeleteMapping}, etc annotations
* rather than {@link ReadOperation @ReadOperation},
* {@link WriteOperation @WriteOperation}, {@link DeleteOperation @DeleteOperation}.
* <p>
* This annotation can be used when deeper Spring integration is required, but at the
* expense of portability. Most users should prefer the {@link Endpoint @Endpoint} or
* {@link WebEndpoint @WebEndpoint} annotations whenever possible.
*
* @author Phillip Webb
* @since 2.0.0
* @see WebEndpoint
* @see ControllerEndpoint
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Endpoint
@FilteredEndpoint(ControllerEndpointFilter.class)
@ResponseBody
public @interface RestControllerEndpoint {
/**
* The id of the endpoint.
* @return the id
*/
@AliasFor(annotation = Endpoint.class)
String id();
/**
* If the endpoint should be enabled or disabled by default.
* @return {@code true} if the endpoint is enabled by default
*/
@AliasFor(annotation = Endpoint.class)
boolean enableByDefault() default true;
}

@ -68,11 +68,11 @@ public class WebEndpointDiscoverer
} }
@Override @Override
protected ExposableWebEndpoint createEndpoint(String id, boolean enabledByDefault, protected ExposableWebEndpoint createEndpoint(Object endpointBean, String id,
Collection<WebOperation> operations) { boolean enabledByDefault, Collection<WebOperation> operations) {
String rootPath = this.endpointPathMapper.getRootPath(id); String rootPath = this.endpointPathMapper.getRootPath(id);
return new DiscoveredWebEndpoint(this, id, rootPath, enabledByDefault, return new DiscoveredWebEndpoint(this, endpointBean, id, rootPath,
operations); enabledByDefault, operations);
} }
@Override @Override

@ -0,0 +1,124 @@
/*
* Copyright 2012-2018 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.web.reactive;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.util.Assert;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.condition.PatternsRequestCondition;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
/**
* {@link HandlerMapping} that exposes {@link ControllerEndpoint @ControllerEndpoint} and
* {@link RestControllerEndpoint @RestControllerEndpoint} annotated endpoints over Spring
* WebFlux.
*
* @author Phillip Webb
* @since 2.0.0
*/
public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping {
private final EndpointMapping endpointMapping;
private final CorsConfiguration corsConfiguration;
private final Map<Object, ExposableControllerEndpoint> handlers;
/**
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
* for the specified endpoints.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints operations
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints,
CorsConfiguration corsConfiguration) {
Assert.notNull(endpointMapping, "EndpointMapping must not be null");
Assert.notNull(endpoints, "Endpoints must not be null");
this.endpointMapping = endpointMapping;
this.handlers = getHandlers(endpoints);
this.corsConfiguration = corsConfiguration;
setOrder(-100);
}
private Map<Object, ExposableControllerEndpoint> getHandlers(
Collection<ExposableControllerEndpoint> endpoints) {
Map<Object, ExposableControllerEndpoint> handlers = new LinkedHashMap<>();
endpoints.stream()
.forEach((endpoint) -> handlers.put(endpoint.getController(), endpoint));
return Collections.unmodifiableMap(handlers);
}
@Override
protected void initHandlerMethods() {
this.handlers.keySet().forEach(this::detectHandlerMethods);
}
@Override
protected void registerHandlerMethod(Object handler, Method method,
RequestMappingInfo mapping) {
ExposableControllerEndpoint endpoint = this.handlers.get(handler);
mapping = withEndpointMappedPatterns(endpoint, mapping);
super.registerHandlerMethod(handler, method, mapping);
}
private RequestMappingInfo withEndpointMappedPatterns(
ExposableControllerEndpoint endpoint, RequestMappingInfo mapping) {
Set<PathPattern> patterns = mapping.getPatternsCondition().getPatterns();
PathPattern[] endpointMappedPatterns = patterns.stream()
.map((pattern) -> getEndpointMappedPattern(endpoint, pattern))
.toArray(PathPattern[]::new);
return withNewPatterns(mapping, endpointMappedPatterns);
}
private PathPattern getEndpointMappedPattern(ExposableControllerEndpoint endpoint,
PathPattern pattern) {
return getPathPatternParser().parse(
this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern));
}
private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping,
PathPattern[] patterns) {
PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
patterns);
return new RequestMappingInfo(patternsCondition, mapping.getMethodsCondition(),
mapping.getParamsCondition(), mapping.getHeadersCondition(),
mapping.getConsumesCondition(), mapping.getProducesCondition(),
mapping.getCustomCondition());
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
}

@ -42,13 +42,11 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping; import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition; import org.springframework.web.servlet.mvc.condition.ConsumesRequestCondition;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition; import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition; import org.springframework.web.servlet.mvc.condition.ProducesRequestCondition;
@ -302,23 +300,4 @@ public abstract class AbstractWebMvcEndpointHandlerMapping
} }
/**
* {@link HandlerInterceptorAdapter} to ensure that
* {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints.
*/
private static final class SkipPathExtensionContentNegotiation
extends HandlerInterceptorAdapter {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(SKIP_ATTRIBUTE, Boolean.TRUE);
return true;
}
}
} }

@ -0,0 +1,130 @@
/*
* Copyright 2012-2018 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.web.servlet;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.util.Assert;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.condition.PatternsRequestCondition;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* {@link HandlerMapping} that exposes {@link ControllerEndpoint @ControllerEndpoint} and
* {@link RestControllerEndpoint @RestControllerEndpoint} annotated endpoints over Spring
* MVC.
*
* @author Phillip Webb
* @since 2.0.0
*/
public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping {
private final EndpointMapping endpointMapping;
private final CorsConfiguration corsConfiguration;
private final Map<Object, ExposableControllerEndpoint> handlers;
/**
* Create a new {@link ControllerEndpointHandlerMapping} instance providing mappings
* for the specified endpoints.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints operations
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints,
CorsConfiguration corsConfiguration) {
Assert.notNull(endpointMapping, "EndpointMapping must not be null");
Assert.notNull(endpoints, "Endpoints must not be null");
this.endpointMapping = endpointMapping;
this.handlers = getHandlers(endpoints);
this.corsConfiguration = corsConfiguration;
setOrder(-100);
setUseSuffixPatternMatch(false);
}
private Map<Object, ExposableControllerEndpoint> getHandlers(
Collection<ExposableControllerEndpoint> endpoints) {
Map<Object, ExposableControllerEndpoint> handlers = new LinkedHashMap<>();
endpoints.stream()
.forEach((endpoint) -> handlers.put(endpoint.getController(), endpoint));
return Collections.unmodifiableMap(handlers);
}
@Override
protected void initHandlerMethods() {
this.handlers.keySet().forEach(this::detectHandlerMethods);
}
@Override
protected void registerHandlerMethod(Object handler, Method method,
RequestMappingInfo mapping) {
ExposableControllerEndpoint endpoint = this.handlers.get(handler);
mapping = withEndpointMappedPatterns(endpoint, mapping);
super.registerHandlerMethod(handler, method, mapping);
}
private RequestMappingInfo withEndpointMappedPatterns(
ExposableControllerEndpoint endpoint, RequestMappingInfo mapping) {
Set<String> patterns = mapping.getPatternsCondition().getPatterns();
String[] endpointMappedPatterns = patterns.stream()
.map((pattern) -> getEndpointMappedPattern(endpoint, pattern))
.toArray(String[]::new);
return withNewPatterns(mapping, endpointMappedPatterns);
}
private String getEndpointMappedPattern(ExposableControllerEndpoint endpoint,
String pattern) {
return this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern);
}
private RequestMappingInfo withNewPatterns(RequestMappingInfo mapping,
String[] patterns) {
PatternsRequestCondition patternsCondition = new PatternsRequestCondition(
patterns, null, null, useSuffixPatternMatch(), useTrailingSlashMatch(),
null);
return new RequestMappingInfo(patternsCondition, mapping.getMethodsCondition(),
mapping.getParamsCondition(), mapping.getHeadersCondition(),
mapping.getConsumesCondition(), mapping.getProducesCondition(),
mapping.getCustomCondition());
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method,
RequestMappingInfo mapping) {
return this.corsConfiguration;
}
@Override
protected void extendInterceptors(List<Object> interceptors) {
interceptors.add(new SkipPathExtensionContentNegotiation());
}
}

@ -0,0 +1,43 @@
/*
* Copyright 2012-2018 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.web.servlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.accept.PathExtensionContentNegotiationStrategy;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
/**
* {@link HandlerInterceptorAdapter} to ensure that
* {@link PathExtensionContentNegotiationStrategy} is skipped for web endpoints.
*
* @author Phillip Webb
*/
final class SkipPathExtensionContentNegotiation extends HandlerInterceptorAdapter {
private static final String SKIP_ATTRIBUTE = PathExtensionContentNegotiationStrategy.class
.getName() + ".SKIP";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response,
Object handler) throws Exception {
request.setAttribute(SKIP_ATTRIBUTE, Boolean.TRUE);
return true;
}
}

@ -477,9 +477,10 @@ public class EndpointDiscovererTests {
} }
@Override @Override
protected TestExposableEndpoint createEndpoint(String id, protected TestExposableEndpoint createEndpoint(Object endpointBean, String id,
boolean enabledByDefault, Collection<TestOperation> operations) { boolean enabledByDefault, Collection<TestOperation> operations) {
return new TestExposableEndpoint(this, id, enabledByDefault, operations); return new TestExposableEndpoint(this, endpointBean, id, enabledByDefault,
operations);
} }
@Override @Override
@ -510,10 +511,11 @@ public class EndpointDiscovererTests {
} }
@Override @Override
protected SpecializedExposableEndpoint createEndpoint(String id, protected SpecializedExposableEndpoint createEndpoint(Object endpointBean,
boolean enabledByDefault, Collection<SpecializedOperation> operations) { String id, boolean enabledByDefault,
return new SpecializedExposableEndpoint(this, id, enabledByDefault, Collection<SpecializedOperation> operations) {
operations); return new SpecializedExposableEndpoint(this, endpointBean, id,
enabledByDefault, operations);
} }
@Override @Override
@ -532,10 +534,10 @@ public class EndpointDiscovererTests {
static class TestExposableEndpoint extends AbstractDiscoveredEndpoint<TestOperation> { static class TestExposableEndpoint extends AbstractDiscoveredEndpoint<TestOperation> {
TestExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, String id, TestExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, Object endpointBean,
boolean enabledByDefault, String id, boolean enabledByDefault,
Collection<? extends TestOperation> operations) { Collection<? extends TestOperation> operations) {
super(discoverer, id, enabledByDefault, operations); super(discoverer, endpointBean, id, enabledByDefault, operations);
} }
} }
@ -543,10 +545,10 @@ public class EndpointDiscovererTests {
static class SpecializedExposableEndpoint static class SpecializedExposableEndpoint
extends AbstractDiscoveredEndpoint<SpecializedOperation> { extends AbstractDiscoveredEndpoint<SpecializedOperation> {
SpecializedExposableEndpoint(EndpointDiscoverer<?, ?> discoverer, String id, SpecializedExposableEndpoint(EndpointDiscoverer<?, ?> discoverer,
boolean enabledByDefault, Object endpointBean, String id, boolean enabledByDefault,
Collection<? extends SpecializedOperation> operations) { Collection<? extends SpecializedOperation> operations) {
super(discoverer, id, enabledByDefault, operations); super(discoverer, endpointBean, id, enabledByDefault, operations);
} }
} }

@ -0,0 +1,149 @@
/*
* Copyright 2012-2018 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.web.annotation;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ControllerEndpointDiscoverer}.
*
* @author Phillip Webb
*/
public class ControllerEndpointDiscovererTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
@Test
public void getEndpointsWhenNoEndpointBeansShouldReturnEmptyCollection() {
load(EmptyConfiguration.class,
(discoverer) -> assertThat(discoverer.getEndpoints()).isEmpty());
}
@Test
public void getEndpointsShouldIncludeControllerEndpoints() {
load(TestControllerEndpoint.class, (discoverer) -> {
Collection<ExposableControllerEndpoint> endpoints = discoverer.getEndpoints();
assertThat(endpoints).hasSize(1);
ExposableControllerEndpoint endpoint = endpoints.iterator().next();
assertThat(endpoint.getId()).isEqualTo("testcontroller");
assertThat(endpoint.getController())
.isInstanceOf(TestControllerEndpoint.class);
});
}
@Test
public void getEndpointsShouldIncludeRestControllerEndpoints() {
load(TestRestControllerEndpoint.class, (discoverer) -> {
Collection<ExposableControllerEndpoint> endpoints = discoverer.getEndpoints();
assertThat(endpoints).hasSize(1);
ExposableControllerEndpoint endpoint = endpoints.iterator().next();
assertThat(endpoint.getId()).isEqualTo("testrestcontroller");
assertThat(endpoint.getController())
.isInstanceOf(TestRestControllerEndpoint.class);
});
}
@Test
public void getEndpointsShouldNotDiscoverRegularEndpoints() {
load(WithRegularEndpointConfiguration.class, (discoverer) -> {
Collection<ExposableControllerEndpoint> endpoints = discoverer.getEndpoints();
List<String> ids = endpoints.stream().map(ExposableEndpoint::getId)
.collect(Collectors.toList());
assertThat(ids).containsOnly("testcontroller", "testrestcontroller");
});
}
@Test
public void getEndpointWhenEndpointHasOperationsShouldThrowException() {
load(TestControllerWithOperation.class, (discoverer) -> {
this.thrown.expect(IllegalStateException.class);
this.thrown.expectMessage("ControllerEndpoints must not declare operations");
discoverer.getEndpoints();
});
}
private void load(Class<?> configuration,
Consumer<ControllerEndpointDiscoverer> consumer) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
configuration);
try {
ControllerEndpointDiscoverer discoverer = new ControllerEndpointDiscoverer(
context, PathMapper.useEndpointId(), Collections.emptyList());
consumer.accept(discoverer);
}
finally {
context.close();
}
}
@Configuration
static class EmptyConfiguration {
}
@Configuration
@Import({ TestEndpoint.class, TestControllerEndpoint.class,
TestRestControllerEndpoint.class })
static class WithRegularEndpointConfiguration {
}
@ControllerEndpoint(id = "testcontroller")
static class TestControllerEndpoint {
}
@RestControllerEndpoint(id = "testrestcontroller")
static class TestRestControllerEndpoint {
}
@Endpoint(id = "test")
static class TestEndpoint {
}
@ControllerEndpoint(id = "testcontroller")
static class TestControllerWithOperation {
@ReadOperation
public String read() {
return "error";
}
}
}

@ -0,0 +1,164 @@
/*
* Copyright 2012-2018 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.web.reactive;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* Integration tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ControllerEndpointHandlerMappingIntegrationTests {
public ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new)
.withUserConfiguration(EndpointConfiguration.class,
ExampleWebFluxEndpoint.class);
@Test
public void get() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.get().uri("/actuator/example/one").accept(MediaType.TEXT_PLAIN)
.exchange().expectStatus().isOk().expectHeader()
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
.expectBody(String.class).isEqualTo("One");
}));
}
@Test
public void getWithUnacceptableContentType() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.get().uri("/actuator/example/one")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.isEqualTo(HttpStatus.NOT_ACCEPTABLE);
}));
}
@Test
public void post() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.post().uri("/actuator/example/two")
.syncBody(Collections.singletonMap("id", "test")).exchange()
.expectStatus().isCreated().expectHeader()
.valueEquals(HttpHeaders.LOCATION, "/example/test");
}));
}
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(
Consumer<WebTestClient> webClient) {
return (context) -> {
int port = ((AnnotationConfigReactiveWebServerApplicationContext) context
.getSourceApplicationContext()).getWebServer().getPort();
WebTestClient webTestClient = createWebTestClient(port);
webClient.accept(webTestClient);
};
}
private WebTestClient createWebTestClient(int port) {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(
"http://localhost:" + port);
uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory)
.responseTimeout(Duration.ofMinutes(2)).build();
}
@Configuration
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class,
WebFluxAutoConfiguration.class })
static class EndpointConfiguration {
@Bean
public NettyReactiveWebServerFactory netty() {
return new NettyReactiveWebServerFactory(0);
}
@Bean
public HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
}
@Bean
public ControllerEndpointDiscoverer webEndpointDiscoverer(
ApplicationContext applicationContext) {
return new ControllerEndpointDiscoverer(applicationContext,
PathMapper.useEndpointId(), Collections.emptyList());
}
@Bean
public ControllerEndpointHandlerMapping webEndpointHandlerMapping(
ControllerEndpointsSupplier endpointsSupplier) {
return new ControllerEndpointHandlerMapping(new EndpointMapping("actuator"),
endpointsSupplier.getEndpoints(), null);
}
}
@RestControllerEndpoint(id = "example")
public static class ExampleWebFluxEndpoint {
@GetMapping(path = "one", produces = MediaType.TEXT_PLAIN_VALUE)
public String one() {
return "One";
}
@PostMapping("/two")
public ResponseEntity<String> two(@RequestBody Map<String, Object> content) {
return ResponseEntity.created(URI.create("/example/" + content.get("id")))
.build();
}
}
}

@ -0,0 +1,149 @@
/*
* Copyright 2012-2018 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.web.reactive;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.MethodNotAllowedException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ControllerEndpointHandlerMappingTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private final StaticApplicationContext context = new StaticApplicationContext();
@Test
public void mappingWithNoPrefix() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("", first, second);
assertThat(getHandler(mapping, HttpMethod.GET, "/first"))
.isEqualTo(handlerOf(first.getController(), "get"));
assertThat(getHandler(mapping, HttpMethod.POST, "/second"))
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(getHandler(mapping, HttpMethod.GET, "/third")).isNull();
}
@Test
public void mappingWithPrefix() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first,
second);
assertThat(getHandler(mapping, HttpMethod.GET, "/actuator/first"))
.isEqualTo(handlerOf(first.getController(), "get"));
assertThat(getHandler(mapping, HttpMethod.POST, "/actuator/second"))
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(getHandler(mapping, HttpMethod.GET, "/first")).isNull();
assertThat(getHandler(mapping, HttpMethod.GET, "/second")).isNull();
}
@Test
public void mappingNarrowedToMethod() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first);
this.thrown.expect(MethodNotAllowedException.class);
getHandler(mapping, HttpMethod.POST, "/actuator/first");
}
private Object getHandler(ControllerEndpointHandlerMapping mapping, HttpMethod method,
String requestURI) {
return mapping.getHandler(exchange(method, requestURI)).block();
}
private ControllerEndpointHandlerMapping createMapping(String prefix,
ExposableControllerEndpoint... endpoints) {
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(
new EndpointMapping(prefix), Arrays.asList(endpoints), null);
mapping.setApplicationContext(this.context);
mapping.afterPropertiesSet();
return mapping;
}
private HandlerMethod handlerOf(Object source, String methodName) {
return new HandlerMethod(source,
ReflectionUtils.findMethod(source.getClass(), methodName));
}
private MockServerWebExchange exchange(HttpMethod method, String requestURI) {
return MockServerWebExchange
.from(MockServerHttpRequest.method(method, requestURI).build());
}
private ExposableControllerEndpoint firstEndpoint() {
return mockEndpoint("first", new FirstTestMvcEndpoint());
}
private ExposableControllerEndpoint secondEndpoint() {
return mockEndpoint("second", new SecondTestMvcEndpoint());
}
private ExposableControllerEndpoint mockEndpoint(String id, Object controller) {
ExposableControllerEndpoint endpoint = mock(ExposableControllerEndpoint.class);
given(endpoint.getId()).willReturn(id);
given(endpoint.getController()).willReturn(controller);
given(endpoint.getRootPath()).willReturn(id);
return endpoint;
}
@ControllerEndpoint(id = "first")
private static class FirstTestMvcEndpoint {
@GetMapping("/")
public String get() {
return "test";
}
}
@ControllerEndpoint(id = "second")
private static class SecondTestMvcEndpoint {
@PostMapping("/")
public void save() {
}
}
}

@ -0,0 +1,158 @@
/*
* Copyright 2012-2018 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.web.servlet;
import java.net.URI;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import org.junit.Test;
import org.springframework.boot.actuate.endpoint.web.PathMapper;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.boot.test.context.assertj.AssertableWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.util.DefaultUriBuilderFactory;
/**
* Integration tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ControllerEndpointHandlerMappingIntegrationTests {
public WebApplicationContextRunner contextRunner = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withUserConfiguration(EndpointConfiguration.class,
ExampleMvcEndpoint.class);
@Test
public void get() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.get().uri("/actuator/example/one").accept(MediaType.TEXT_PLAIN)
.exchange().expectStatus().isOk().expectHeader()
.contentTypeCompatibleWith(MediaType.TEXT_PLAIN)
.expectBody(String.class).isEqualTo("One");
}));
}
@Test
public void getWithUnacceptableContentType() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.get().uri("/actuator/example/one")
.accept(MediaType.APPLICATION_JSON).exchange().expectStatus()
.isEqualTo(HttpStatus.NOT_ACCEPTABLE);
}));
}
@Test
public void post() {
this.contextRunner.run(withWebTestClient(webTestClient -> {
webTestClient.post().uri("/actuator/example/two")
.syncBody(Collections.singletonMap("id", "test")).exchange()
.expectStatus().isCreated().expectHeader()
.valueEquals(HttpHeaders.LOCATION, "/example/test");
}));
}
private ContextConsumer<AssertableWebApplicationContext> withWebTestClient(
Consumer<WebTestClient> webClient) {
return (context) -> {
int port = ((AnnotationConfigServletWebServerApplicationContext) context
.getSourceApplicationContext()).getWebServer().getPort();
WebTestClient webTestClient = createWebTestClient(port);
webClient.accept(webTestClient);
};
}
private WebTestClient createWebTestClient(int port) {
DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(
"http://localhost:" + port);
uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE);
return WebTestClient.bindToServer().uriBuilderFactory(uriBuilderFactory)
.responseTimeout(Duration.ofMinutes(2)).build();
}
@Configuration
@ImportAutoConfiguration({ JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, WebMvcAutoConfiguration.class,
DispatcherServletAutoConfiguration.class })
static class EndpointConfiguration {
@Bean
public TomcatServletWebServerFactory tomcat() {
return new TomcatServletWebServerFactory(0);
}
@Bean
public ControllerEndpointDiscoverer webEndpointDiscoverer(
ApplicationContext applicationContext) {
return new ControllerEndpointDiscoverer(applicationContext,
PathMapper.useEndpointId(), Collections.emptyList());
}
@Bean
public ControllerEndpointHandlerMapping webEndpointHandlerMapping(
ControllerEndpointsSupplier endpointsSupplier) {
return new ControllerEndpointHandlerMapping(new EndpointMapping("actuator"),
endpointsSupplier.getEndpoints(), null);
}
}
@RestControllerEndpoint(id = "example")
public static class ExampleMvcEndpoint {
@GetMapping(path = "one", produces = MediaType.TEXT_PLAIN_VALUE)
public String one() {
return "One";
}
@PostMapping("/two")
public ResponseEntity<String> two(@RequestBody Map<String, Object> content) {
return ResponseEntity.created(URI.create("/example/" + content.get("id")))
.build();
}
}
}

@ -0,0 +1,141 @@
/*
* Copyright 2012-2018 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.web.servlet;
import java.util.Arrays;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.boot.endpoint.web.EndpointMapping;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.method.HandlerMethod;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
*/
public class ControllerEndpointHandlerMappingTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
private final StaticApplicationContext context = new StaticApplicationContext();
@Test
public void mappingWithNoPrefix() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("", first, second);
assertThat(mapping.getHandler(request("GET", "/first")).getHandler())
.isEqualTo(handlerOf(first.getController(), "get"));
assertThat(mapping.getHandler(request("POST", "/second")).getHandler())
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(mapping.getHandler(request("GET", "/third"))).isNull();
}
@Test
public void mappingWithPrefix() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first,
second);
assertThat(mapping.getHandler(request("GET", "/actuator/first")).getHandler())
.isEqualTo(handlerOf(first.getController(), "get"));
assertThat(mapping.getHandler(request("POST", "/actuator/second")).getHandler())
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(mapping.getHandler(request("GET", "/first"))).isNull();
assertThat(mapping.getHandler(request("GET", "/second"))).isNull();
}
@Test
public void mappingNarrowedToMethod() throws Exception {
ExposableControllerEndpoint first = firstEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first);
this.thrown.expect(HttpRequestMethodNotSupportedException.class);
mapping.getHandler(request("POST", "/actuator/first"));
}
private ControllerEndpointHandlerMapping createMapping(String prefix,
ExposableControllerEndpoint... endpoints) {
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(
new EndpointMapping(prefix), Arrays.asList(endpoints), null);
mapping.setApplicationContext(this.context);
mapping.afterPropertiesSet();
return mapping;
}
private HandlerMethod handlerOf(Object source, String methodName) {
return new HandlerMethod(source,
ReflectionUtils.findMethod(source.getClass(), methodName));
}
private MockHttpServletRequest request(String method, String requestURI) {
return new MockHttpServletRequest(method, requestURI);
}
private ExposableControllerEndpoint firstEndpoint() {
return mockEndpoint("first", new FirstTestMvcEndpoint());
}
private ExposableControllerEndpoint secondEndpoint() {
return mockEndpoint("second", new SecondTestMvcEndpoint());
}
private ExposableControllerEndpoint mockEndpoint(String id, Object controller) {
ExposableControllerEndpoint endpoint = mock(ExposableControllerEndpoint.class);
given(endpoint.getId()).willReturn(id);
given(endpoint.getController()).willReturn(controller);
given(endpoint.getRootPath()).willReturn(id);
return endpoint;
}
@ControllerEndpoint(id = "first")
private static class FirstTestMvcEndpoint {
@GetMapping("/")
public String get() {
return "test";
}
}
@ControllerEndpoint(id = "second")
private static class SecondTestMvcEndpoint {
@PostMapping("/")
public void save() {
}
}
}

@ -43,15 +43,13 @@
<disallow pkg="org.springframework.http" /> <disallow pkg="org.springframework.http" />
<disallow pkg="org.springframework.web" /> <disallow pkg="org.springframework.web" />
<subpackage name="web"> <subpackage name="web">
<subpackage name="servlet">
<disallow pkg="org.springframework.web.reactive" />
<allow pkg="org.springframework.http" /> <allow pkg="org.springframework.http" />
<allow pkg="org.springframework.web" /> <allow pkg="org.springframework.web" />
<subpackage name="servlet">
<disallow pkg="org.springframework.web.reactive" />
</subpackage> </subpackage>
<subpackage name="reactive"> <subpackage name="reactive">
<disallow pkg="org.springframework.web.servlet" /> <disallow pkg="org.springframework.web.servlet" />
<allow pkg="org.springframework.http" />
<allow pkg="org.springframework.web" />
</subpackage> </subpackage>
</subpackage> </subpackage>
</subpackage> </subpackage>

@ -0,0 +1,34 @@
/*
* Copyright 2012-2018 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 sample.actuator.customsecurity;
import org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Component
@RestControllerEndpoint(id = "example")
public class ExampleRestControllerEndpoint {
@GetMapping("/echo")
public ResponseEntity<String> echo(@RequestParam("text") String text) {
return ResponseEntity.ok().header("echo", text).body(text);
}
}

@ -109,6 +109,22 @@ public class SampleActuatorCustomSecurityApplicationTests {
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
} }
@Test
public void actuatorCustomMvcSecureEndpointWithUnauthorizedUser() {
ResponseEntity<String> entity = userRestTemplate()
.getForEntity("/actuator/example/echo?text={t}", String.class, "test");
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
}
@Test
public void actuatorCustomMvcSecureEndpointWithAuthorizedUser() {
ResponseEntity<String> entity = adminRestTemplate()
.getForEntity("/actuator/example/echo?text={t}", String.class, "test");
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody()).isEqualTo("test");
assertThat(entity.getHeaders().getFirst("echo")).isEqualTo("test");
}
private TestRestTemplate restTemplate() { private TestRestTemplate restTemplate() {
return configure(new TestRestTemplate()); return configure(new TestRestTemplate());
} }

Loading…
Cancel
Save