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
This commit is contained in:
Phillip Webb
2018-01-18 20:52:35 -08:00
parent 340ef52f78
commit bda9b892b3
33 changed files with 1882 additions and 61 deletions

View File

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

View File

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

View File

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

View File

@@ -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() {
}
}
}

View File

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

View File

@@ -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() {
}
}
}