Move Actuator infrastructure for WebMvc to spring-boot-webmvc

This commit is contained in:
Stéphane Nicoll
2025-05-29 19:17:12 +02:00
committed by Phillip Webb
parent 272eca17e5
commit 46a5ea0ee7
52 changed files with 395 additions and 299 deletions

View File

@@ -20,18 +20,20 @@ import java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.Access;
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
import org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar;
import org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import static org.assertj.core.api.Assertions.assertThat;
@@ -45,7 +47,9 @@ import static org.assertj.core.api.Assertions.assertThat;
class WebMvcEndpointManagementContextConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withUserConfiguration(TestConfig.class);
.withUserConfiguration(TestConfig.class)
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class));
@Test
void contextShouldContainServletEndpointRegistrar() {
@@ -63,8 +67,7 @@ class WebMvcEndpointManagementContextConfigurationTests {
}
@Configuration(proxyBeanMethods = false)
@Import(WebMvcEndpointManagementContextConfiguration.class)
@EnableConfigurationProperties(WebEndpointProperties.class)
@ImportAutoConfiguration(WebMvcEndpointManagementContextConfiguration.class)
static class TestConfig {
@Bean

View File

@@ -0,0 +1,84 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.health;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.WithTestEndpointOutcomeExposureContributor;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointWebExtension;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.webmvc.actuate.endpoint.web.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebMvcHealthEndpointExtensionAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class WebMvcHealthEndpointExtensionAutoConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, WebMvcHealthEndpointExtensionAutoConfiguration.class));
@Test
@WithTestEndpointOutcomeExposureContributor
void additionalHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class))
.withBean(DispatcherServlet.class)
.withPropertyValues("management.endpoints.web.exposure.exclude=*",
"management.endpoints.test.exposure.include=*")
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(HealthEndpoint.class);
assertThat(context).hasSingleBean(HealthEndpointWebExtension.class);
assertThat(context.getBean(WebEndpointsSupplier.class).getEndpoints()).isEmpty();
assertThat(context).hasSingleBean(AdditionalHealthEndpointPathsWebMvcHandlerMapping.class);
});
}
@Configuration(proxyBeanMethods = false)
static class HealthIndicatorsConfiguration {
@Bean
HealthIndicator simpleHealthIndicator() {
return () -> Health.up().withDetail("counter", 42).build();
}
@Bean
HealthIndicator additionalHealthIndicator() {
return () -> Health.up().build();
}
}
}

View File

@@ -0,0 +1,109 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.web;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link CompositeHandlerExceptionResolver}.
*
* @author Madhura Bhave
* @author Scott Frederick
*/
class CompositeHandlerExceptionResolverTests {
private AnnotationConfigApplicationContext context;
private final MockHttpServletRequest request = new MockHttpServletRequest();
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
void resolverShouldDelegateToOtherResolversInContext() {
load(TestConfiguration.class);
CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context
.getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME);
ModelAndView resolved = resolver.resolveException(this.request, this.response, null,
new HttpRequestMethodNotSupportedException("POST"));
assertThat(resolved.getViewName()).isEqualTo("test-view");
}
@Test
void resolverShouldAddDefaultResolverIfNonePresent() {
load(BaseConfiguration.class);
CompositeHandlerExceptionResolver resolver = (CompositeHandlerExceptionResolver) this.context
.getBean(DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME);
HttpRequestMethodNotSupportedException exception = new HttpRequestMethodNotSupportedException("POST");
ModelAndView resolved = resolver.resolveException(this.request, this.response, null, exception);
assertThat(resolved).isNotNull();
assertThat(resolved.isEmpty()).isTrue();
}
private void load(Class<?>... configs) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(configs);
context.refresh();
this.context = context;
}
@Configuration(proxyBeanMethods = false)
static class BaseConfiguration {
@Bean(name = DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME)
CompositeHandlerExceptionResolver compositeHandlerExceptionResolver() {
return new CompositeHandlerExceptionResolver();
}
}
@Configuration(proxyBeanMethods = false)
@Import(BaseConfiguration.class)
static class TestConfiguration {
@Bean
HandlerExceptionResolver testResolver() {
return new TestHandlerExceptionResolver();
}
}
static class TestHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
return new ModelAndView("test-view");
}
}
}

View File

@@ -0,0 +1,167 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.web;
import java.util.Collections;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.webmvc.error.DefaultErrorAttributes;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.context.request.WebRequest;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
/**
* Tests for {@link ManagementErrorEndpoint}.
*
* @author Scott Frederick
*/
class ManagementErrorEndpointTests {
private final ErrorAttributes errorAttributes = new DefaultErrorAttributes();
private final ErrorProperties errorProperties = new ErrorProperties();
private final MockHttpServletRequest request = new MockHttpServletRequest();
@BeforeEach
void setUp() {
this.request.setAttribute("jakarta.servlet.error.exception", new RuntimeException("test exception"));
}
@Test
void errorResponseNeverDetails() {
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest()));
assertThat(response).doesNotContainKey("message");
assertThat(response).doesNotContainKey("trace");
}
@Test
void errorResponseAlwaysDetails() {
this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ALWAYS);
this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ALWAYS);
this.request.addParameter("trace", "false");
this.request.addParameter("message", "false");
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(this.request));
assertThat(response).containsEntry("message", "test exception");
assertThat(response).hasEntrySatisfying("trace",
(value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception"));
}
@Test
void errorResponseParamsAbsent() {
this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM);
this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM);
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(this.request));
assertThat(response).doesNotContainKey("message");
assertThat(response).doesNotContainKey("trace");
}
@Test
void errorResponseParamsTrue() {
this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM);
this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM);
this.request.addParameter("trace", "true");
this.request.addParameter("message", "true");
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(this.request));
assertThat(response).containsEntry("message", "test exception");
assertThat(response).hasEntrySatisfying("trace",
(value) -> assertThat(value).asString().startsWith("java.lang.RuntimeException: test exception"));
}
@Test
void errorResponseParamsFalse() {
this.errorProperties.setIncludeStacktrace(ErrorProperties.IncludeAttribute.ON_PARAM);
this.errorProperties.setIncludeMessage(ErrorProperties.IncludeAttribute.ON_PARAM);
this.request.addParameter("trace", "false");
this.request.addParameter("message", "false");
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(this.errorAttributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(this.request));
assertThat(response).doesNotContainKey("message");
assertThat(response).doesNotContainKey("trace");
}
@Test
void errorResponseWithCustomErrorAttributesUsingDeprecatedApi() {
ErrorAttributes attributes = new ErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
return Collections.singletonMap("message", "An error occurred");
}
@Override
public Throwable getError(WebRequest webRequest) {
return null;
}
};
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest()));
assertThat(response).containsExactly(entry("message", "An error occurred"));
}
@Test
void errorResponseWithDefaultErrorAttributesSubclassUsingDelegation() {
ErrorAttributes attributes = new DefaultErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> response = super.getErrorAttributes(webRequest, options);
response.put("error", "custom error");
response.put("custom", "value");
response.remove("path");
return response;
}
};
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest()));
assertThat(response).containsEntry("error", "custom error");
assertThat(response).containsEntry("custom", "value");
assertThat(response).doesNotContainKey("path");
assertThat(response).containsKey("timestamp");
}
@Test
void errorResponseWithDefaultErrorAttributesSubclassWithoutDelegation() {
ErrorAttributes attributes = new DefaultErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
return Collections.singletonMap("error", "custom error");
}
};
ManagementErrorEndpoint endpoint = new ManagementErrorEndpoint(attributes, this.errorProperties);
Map<String, Object> response = endpoint.invoke(new ServletWebRequest(new MockHttpServletRequest()));
assertThat(response).containsExactly(entry("error", "custom error"));
}
}

View File

@@ -0,0 +1,247 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import java.util.function.Consumer;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
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.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.env.ConfigTreePropertySource;
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.tomcat.actuate.autoconfigure.web.TomcatServletManagementContextAutoConfiguration;
import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration;
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.server.servlet.context.AnnotationConfigServletWebServerApplicationContext;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
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.bind.annotation.ResponseBody;
import org.springframework.web.client.RestClient;
import org.springframework.web.client.RestClient.RequestHeadersSpec.ExchangeFunction;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link WebMvcEndpointChildContextConfiguration}.
*
* @author Phillip Webb
* @author Scott Frederick
*/
class WebMvcEndpointChildContextConfigurationIntegrationTests {
private final WebApplicationContextRunner runner = new WebApplicationContextRunner(
AnnotationConfigServletWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class,
TomcatServletWebServerAutoConfiguration.class, TomcatServletManagementContextAutoConfiguration.class,
ServletManagementContextAutoConfiguration.class, WebEndpointAutoConfiguration.class,
EndpointAutoConfiguration.class, DispatcherServletAutoConfiguration.class,
ErrorMvcAutoConfiguration.class))
.withUserConfiguration(SucceedingEndpoint.class, FailingEndpoint.class, FailingControllerEndpoint.class)
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withPropertyValues("server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*",
"server.error.include-exception=true", "server.error.include-message=always",
"server.error.include-binding-errors=always");
@TempDir
Path temp;
@Test // gh-17938
void errorEndpointIsUsedWithEndpoint() {
this.runner.run(withRestClient((client) -> {
Map<String, ?> body = client.get()
.uri("actuator/fail")
.accept(MediaType.APPLICATION_JSON)
.exchange(toResponseBody());
assertThat(body).hasEntrySatisfying("exception",
(value) -> assertThat(value).asString().contains("IllegalStateException"));
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Epic Fail"));
}));
}
@Test
void errorPageAndErrorControllerIncludeDetails() {
this.runner.withPropertyValues("server.error.include-stacktrace=always", "server.error.include-message=always")
.run(withRestClient((client) -> {
Map<String, ?> body = client.get()
.uri("actuator/fail")
.accept(MediaType.APPLICATION_JSON)
.exchange(toResponseBody());
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Epic Fail"));
assertThat(body).hasEntrySatisfying("trace",
(value) -> assertThat(value).asString().contains("java.lang.IllegalStateException: Epic Fail"));
}));
}
@Test
void errorEndpointIsUsedWithRestControllerEndpoint() {
this.runner.run(withRestClient((client) -> {
Map<String, ?> body = client.get()
.uri("actuator/failController")
.accept(MediaType.APPLICATION_JSON)
.exchange(toResponseBody());
assertThat(body).hasEntrySatisfying("exception",
(value) -> assertThat(value).asString().contains("IllegalStateException"));
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Epic Fail"));
}));
}
@Test
void errorEndpointIsUsedWithRestControllerEndpointOnBindingError() {
this.runner.run(withRestClient((client) -> {
Map<String, ?> body = client.post()
.uri("actuator/failController")
.body(Collections.singletonMap("content", ""))
.accept(MediaType.APPLICATION_JSON)
.exchange(toResponseBody());
assertThat(body).hasEntrySatisfying("exception",
(value) -> assertThat(value).asString().contains("MethodArgumentNotValidException"));
assertThat(body).hasEntrySatisfying("message",
(value) -> assertThat(value).asString().contains("Validation failed"));
assertThat(body).hasEntrySatisfying("errors",
(value) -> assertThat(value).asInstanceOf(InstanceOfAssertFactories.LIST).isNotEmpty());
}));
}
@Test
void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() {
this.runner.withPropertyValues("management.server.base-path:/manage").run(withRestClient((client) -> {
String body = client.get()
.uri("manage/actuator/success")
.accept(MediaType.APPLICATION_JSON)
.retrieve()
.body(String.class);
assertThat(body).isEqualTo("Success");
}));
}
@Test // gh-32941
void whenManagementServerPortLoadedFromConfigTree() {
this.runner.withInitializer(this::addConfigTreePropertySource)
.run((context) -> assertThat(context).hasNotFailed());
}
private void addConfigTreePropertySource(ConfigurableApplicationContext applicationContext) {
try {
applicationContext.getEnvironment()
.setConversionService((ConfigurableConversionService) ApplicationConversionService.getSharedInstance());
Path configtree = this.temp.resolve("configtree");
Path file = configtree.resolve("management/server/port");
file.toFile().getParentFile().mkdirs();
FileCopyUtils.copy("0".getBytes(StandardCharsets.UTF_8), file.toFile());
ConfigTreePropertySource source = new ConfigTreePropertySource("configtree", configtree);
applicationContext.getEnvironment().getPropertySources().addFirst(source);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private ContextConsumer<AssertableWebApplicationContext> withRestClient(Consumer<RestClient> restClient) {
return (context) -> {
String port = context.getEnvironment().getProperty("local.management.port");
RestClient client = RestClient.create("http://localhost:" + port);
restClient.accept(client);
};
}
private ExchangeFunction<Map<String, ?>> toResponseBody() {
return ((request, response) -> response.bodyTo(new ParameterizedTypeReference<Map<String, ?>>() {
}));
}
@Endpoint(id = "fail")
static class FailingEndpoint {
@ReadOperation
String fail() {
throw new IllegalStateException("Epic Fail");
}
}
@Endpoint(id = "success")
static class SucceedingEndpoint {
@ReadOperation
String fail() {
return "Success";
}
}
@org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint(id = "failController")
@SuppressWarnings("removal")
static class FailingControllerEndpoint {
@GetMapping
String fail() {
throw new IllegalStateException("Epic Fail");
}
@PostMapping(produces = "application/json")
@ResponseBody
String bodyValidation(@Valid @RequestBody TestBody body) {
return body.getContent();
}
}
public static class TestBody {
@NotEmpty
private String content;
public String getContent() {
return this.content;
}
public void setContent(String content) {
this.content = content;
}
}
}

View File

@@ -0,0 +1,92 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.web;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.filter.RequestContextFilter;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebMvcEndpointChildContextConfiguration}.
*
* @author Madhura Bhave
*/
class WebMvcEndpointChildContextConfigurationTests {
private final WebApplicationContextRunner contextRunner = new WebApplicationContextRunner()
.withAllowBeanDefinitionOverriding(true);
@Test
void contextShouldConfigureRequestContextFilter() {
this.contextRunner.withUserConfiguration(WebMvcEndpointChildContextConfiguration.class)
.run((context) -> assertThat(context).hasSingleBean(OrderedRequestContextFilter.class));
}
@Test
void contextShouldNotConfigureRequestContextFilterWhenPresent() {
this.contextRunner.withUserConfiguration(ExistingConfig.class, WebMvcEndpointChildContextConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(RequestContextFilter.class);
assertThat(context).hasBean("testRequestContextFilter");
});
}
@Test
void contextShouldNotConfigureRequestContextFilterWhenRequestContextListenerPresent() {
this.contextRunner
.withUserConfiguration(RequestContextListenerConfig.class, WebMvcEndpointChildContextConfiguration.class)
.run((context) -> {
assertThat(context).hasSingleBean(RequestContextListener.class);
assertThat(context).doesNotHaveBean(OrderedRequestContextFilter.class);
});
}
@Test
void contextShouldConfigureDispatcherServletPathWithRootPath() {
this.contextRunner.withUserConfiguration(WebMvcEndpointChildContextConfiguration.class)
.run((context) -> assertThat(context.getBean(DispatcherServletPath.class).getPath()).isEqualTo("/"));
}
@Configuration(proxyBeanMethods = false)
static class ExistingConfig {
@Bean
RequestContextFilter testRequestContextFilter() {
return new RequestContextFilter();
}
}
@Configuration(proxyBeanMethods = false)
static class RequestContextListenerConfig {
@Bean
RequestContextListener testRequestContextListener() {
return new RequestContextListener();
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.endpoint.web;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.webmvc.actuate.endpoint.web.AbstractWebMvcEndpointHandlerMapping.AbstractWebMvcEndpointHandlerMappingRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AbstractWebMvcEndpointHandlerMapping}.
*
* @author Moritz Halbritter
*/
class AbstractWebMvcEndpointHandlerMappingTests {
@Test
void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints();
new AbstractWebMvcEndpointHandlerMappingRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection()
.onType(TypeReference
.of("org.springframework.boot.webmvc.actuate.endpoint.web.AbstractWebMvcEndpointHandlerMapping.OperationHandler")))
.accepts(runtimeHints);
}
}

View File

@@ -0,0 +1,163 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.endpoint.web;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.Access;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
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.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support
*/
@Deprecated(since = "3.3.5", forRemoval = true)
@SuppressWarnings("removal")
class ControllerEndpointHandlerMappingTests {
private final StaticApplicationContext context = new StaticApplicationContext();
@Test
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
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
void mappingNarrowedToMethod() {
ExposableControllerEndpoint first = firstEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first);
assertThatExceptionOfType(HttpRequestMethodNotSupportedException.class)
.isThrownBy(() -> mapping.getHandler(request("POST", "/actuator/first")));
}
@Test
void mappingWithNoPath() throws Exception {
ExposableControllerEndpoint pathless = pathlessEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", pathless);
assertThat(mapping.getHandler(request("GET", "/actuator/pathless")).getHandler())
.isEqualTo(handlerOf(pathless.getController(), "get"));
assertThat(mapping.getHandler(request("GET", "/pathless"))).isNull();
assertThat(mapping.getHandler(request("GET", "/"))).isNull();
}
private ControllerEndpointHandlerMapping createMapping(String prefix, ExposableControllerEndpoint... endpoints) {
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(new EndpointMapping(prefix),
Arrays.asList(endpoints), null, (endpointId, defaultAccess) -> Access.UNRESTRICTED);
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(EndpointId.of("first"), new FirstTestMvcEndpoint());
}
private ExposableControllerEndpoint secondEndpoint() {
return mockEndpoint(EndpointId.of("second"), new SecondTestMvcEndpoint());
}
private ExposableControllerEndpoint pathlessEndpoint() {
return mockEndpoint(EndpointId.of("pathless"), new PathlessControllerEndpoint());
}
private ExposableControllerEndpoint mockEndpoint(EndpointId id, Object controller) {
ExposableControllerEndpoint endpoint = mock(ExposableControllerEndpoint.class);
given(endpoint.getEndpointId()).willReturn(id);
given(endpoint.getController()).willReturn(controller);
given(endpoint.getRootPath()).willReturn(id.toString());
return endpoint;
}
@ControllerEndpoint(id = "first")
static class FirstTestMvcEndpoint {
@GetMapping("/")
String get() {
return "test";
}
}
@ControllerEndpoint(id = "second")
static class SecondTestMvcEndpoint {
@PostMapping("/")
void save() {
}
}
@ControllerEndpoint(id = "pathless")
static class PathlessControllerEndpoint {
@GetMapping
String get() {
return "test";
}
}
}

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.webmvc.actuate.endpoint.web;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping.WebMvcEndpointHandlerMappingRuntimeHints;
import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping.WebMvcLinksHandler;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebMvcEndpointHandlerMapping}.
*
* @author Moritz Halbritter
*/
class WebMvcEndpointHandlerMappingTests {
@Test
void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints();
new WebMvcEndpointHandlerMappingRuntimeHints().registerHints(runtimeHints, getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(WebMvcLinksHandler.class, "links"))
.accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection().onType(Link.class)).accepts(runtimeHints);
}
}