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-10257pull/11626/merge
parent
340ef52f78
commit
bda9b892b3
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue