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

@@ -16,23 +16,129 @@
package org.springframework.boot.webmvc.actuate.autoconfigure.endpoint.web;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ConditionalOnManagementPort;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.actuate.endpoint.ExposableEndpoint;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.jackson.EndpointObjectMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.webmvc.actuate.endpoint.web.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletPath;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
* {@link Endpoint @Endpoint} concerns.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Stephane Nicoll
* @since 4.0.0
*/
@ManagementContextConfiguration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
class WebMvcEndpointManagementContextConfiguration {
@ConditionalOnBean({ DispatcherServlet.class, WebEndpointsSupplier.class })
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebMvcEndpointManagementContextConfiguration {
@Bean
@SuppressWarnings({ "deprecation", "removal" })
@ConditionalOnMissingBean
@SuppressWarnings("removal")
WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier,
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties, Environment environment) {
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
allEndpoints.addAll(webEndpoints);
allEndpoints.addAll(servletEndpointsSupplier.getEndpoints());
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping);
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,
String basePath) {
return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
|| ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
AdditionalHealthEndpointPathsWebMvcHandlerMapping managementHealthEndpointWebMvcHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint healthEndpoint = webEndpoints.stream()
.filter(this::isHealthEndpoint)
.findFirst()
.orElse(null);
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(healthEndpoint,
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
}
private boolean isHealthEndpoint(ExposableWebEndpoint endpoint) {
return endpoint.getEndpointId().equals(HealthEndpoint.ID);
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
@Deprecated(since = "3.3.5", forRemoval = true)
org.springframework.boot.webmvc.actuate.endpoint.web.ControllerEndpointHandlerMapping controllerEndpointHandlerMapping(
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
CorsEndpointProperties corsProperties, WebEndpointProperties webEndpointProperties,
EndpointAccessResolver endpointAccessResolver) {
EndpointMapping endpointMapping = new EndpointMapping(webEndpointProperties.getBasePath());
return new org.springframework.boot.webmvc.actuate.endpoint.web.ControllerEndpointHandlerMapping(
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration(),
endpointAccessResolver);
}
@Bean
@SuppressWarnings("removal")
org.springframework.boot.actuate.endpoint.web.ServletEndpointRegistrar servletEndpointRegistrar(
WebEndpointProperties properties,
org.springframework.boot.actuate.endpoint.web.annotation.ServletEndpointsSupplier servletEndpointsSupplier,
@@ -42,4 +148,49 @@ class WebMvcEndpointManagementContextConfiguration {
servletEndpointsSupplier.getEndpoints(), endpointAccessResolver);
}
@Bean
@ConditionalOnBean(EndpointObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static EndpointObjectMapperWebMvcConfigurer endpointObjectMapperWebMvcConfigurer(
EndpointObjectMapper endpointObjectMapper) {
return new EndpointObjectMapperWebMvcConfigurer(endpointObjectMapper);
}
/**
* {@link WebMvcConfigurer} to apply {@link EndpointObjectMapper} for
* {@link OperationResponseBody} to
* {@link org.springframework.http.converter.json.MappingJackson2HttpMessageConverter}
* instances.
*/
@SuppressWarnings("removal")
static class EndpointObjectMapperWebMvcConfigurer implements WebMvcConfigurer {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
private final EndpointObjectMapper endpointObjectMapper;
EndpointObjectMapperWebMvcConfigurer(EndpointObjectMapper endpointObjectMapper) {
this.endpointObjectMapper = endpointObjectMapper;
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (HttpMessageConverter<?> converter : converters) {
if (converter instanceof org.springframework.http.converter.json.MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter) {
configure(mappingJackson2HttpMessageConverter);
}
}
}
@SuppressWarnings("removal")
private void configure(org.springframework.http.converter.json.MappingJackson2HttpMessageConverter converter) {
converter.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
ObjectMapper objectMapper = this.endpointObjectMapper.get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
});
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* 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 java.util.Collection;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnAvailableEndpoint;
import org.springframework.boot.actuate.autoconfigure.endpoint.expose.EndpointExposure;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthEndpointGroups;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.webmvc.actuate.endpoint.web.AdditionalHealthEndpointPathsWebMvcHandlerMapping;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link HealthEndpoint} web
* extension with Spring MVC.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(HealthEndpoint.class)
@ConditionalOnBean(HealthEndpoint.class)
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
public class WebMvcHealthEndpointExtensionAutoConfiguration {
@Bean
public AdditionalHealthEndpointPathsWebMvcHandlerMapping healthEndpointWebMvcHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
ExposableWebEndpoint health = getHealthEndpoint(webEndpointsSupplier);
return new AdditionalHealthEndpointPathsWebMvcHandlerMapping(health,
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
}
private static ExposableWebEndpoint getHealthEndpoint(WebEndpointsSupplier webEndpointsSupplier) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
return webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
.findFirst()
.orElse(null);
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Auto-configuration for actuator health concerns.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.health;

View File

@@ -0,0 +1,77 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.Optional;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.servlet.HandlerAdapter;
import org.springframework.web.servlet.ModelAndView;
/**
* Composite {@link HandlerAdapter}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
*/
class CompositeHandlerAdapter implements HandlerAdapter {
private final ListableBeanFactory beanFactory;
private List<HandlerAdapter> adapters;
CompositeHandlerAdapter(ListableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public boolean supports(Object handler) {
return getAdapter(handler).isPresent();
}
@Override
public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
Optional<HandlerAdapter> adapter = getAdapter(handler);
if (adapter.isPresent()) {
return adapter.get().handle(request, response, handler);
}
return null;
}
private Optional<HandlerAdapter> getAdapter(Object handler) {
if (this.adapters == null) {
this.adapters = extractAdapters();
}
return this.adapters.stream().filter((a) -> a.supports(handler)).findFirst();
}
private List<HandlerAdapter> extractAdapters() {
List<HandlerAdapter> list = new ArrayList<>(this.beanFactory.getBeansOfType(HandlerAdapter.class).values());
list.remove(this);
AnnotationAwareOrderComparator.sort(list);
return list;
}
}

View File

@@ -0,0 +1,88 @@
/*
* 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.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.error.DefaultErrorAttributes;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver;
/**
* Composite {@link HandlerExceptionResolver}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
* @author Scott Frederick
* @author Guirong Hu
*/
class CompositeHandlerExceptionResolver implements HandlerExceptionResolver {
@Autowired
private ListableBeanFactory beanFactory;
private volatile List<HandlerExceptionResolver> resolvers;
@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
for (HandlerExceptionResolver resolver : getResolvers()) {
ModelAndView resolved = resolver.resolveException(request, response, handler, ex);
if (resolved != null) {
return resolved;
}
}
return null;
}
private List<HandlerExceptionResolver> getResolvers() {
List<HandlerExceptionResolver> resolvers = this.resolvers;
if (resolvers == null) {
resolvers = new ArrayList<>();
collectResolverBeans(resolvers, this.beanFactory);
resolvers.remove(this);
AnnotationAwareOrderComparator.sort(resolvers);
if (resolvers.isEmpty()) {
resolvers.add(new DefaultErrorAttributes());
resolvers.add(new DefaultHandlerExceptionResolver());
}
this.resolvers = resolvers;
}
return resolvers;
}
private void collectResolverBeans(List<HandlerExceptionResolver> resolvers, BeanFactory beanFactory) {
if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
resolvers.addAll(listableBeanFactory.getBeansOfType(HandlerExceptionResolver.class).values());
}
if (beanFactory instanceof HierarchicalBeanFactory hierarchicalBeanFactory) {
collectResolverBeans(resolvers, hierarchicalBeanFactory.getParentBeanFactory());
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.ArrayList;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.web.servlet.HandlerExecutionChain;
import org.springframework.web.servlet.HandlerMapping;
/**
* Composite {@link HandlerMapping}.
*
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Phillip Webb
*/
class CompositeHandlerMapping implements HandlerMapping {
@Autowired
private ListableBeanFactory beanFactory;
private List<HandlerMapping> mappings;
@Override
public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
for (HandlerMapping mapping : getMappings()) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
return null;
}
@Override
public boolean usesPathPatterns() {
for (HandlerMapping mapping : getMappings()) {
if (mapping.usesPathPatterns()) {
return true;
}
}
return false;
}
private List<HandlerMapping> getMappings() {
if (this.mappings == null) {
this.mappings = extractMappings();
}
return this.mappings;
}
private List<HandlerMapping> extractMappings() {
List<HandlerMapping> list = new ArrayList<>(this.beanFactory.getBeansOfType(HandlerMapping.class).values());
list.remove(this);
AnnotationAwareOrderComparator.sort(list);
return list;
}
}

View File

@@ -0,0 +1,120 @@
/*
* 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.Map;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.error.ErrorAttributeOptions.Include;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.boot.webmvc.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.util.Assert;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.ServletWebRequest;
/**
* {@link Controller @Controller} for handling "/error" path when the management servlet
* is in a child context. The regular {@link ErrorController} should be available there
* but because of the way the handler mappings are set up it will not be detected.
*
* @author Dave Syer
* @author Scott Frederick
* @author Moritz Halbritter
* @since 2.0.0
*/
@Controller
public class ManagementErrorEndpoint {
private final ErrorAttributes errorAttributes;
private final ErrorProperties errorProperties;
public ManagementErrorEndpoint(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
Assert.notNull(errorAttributes, "'errorAttributes' must not be null");
Assert.notNull(errorProperties, "'errorProperties' must not be null");
this.errorAttributes = errorAttributes;
this.errorProperties = errorProperties;
}
@RequestMapping("${server.error.path:${error.path:/error}}")
@ResponseBody
public Map<String, Object> invoke(ServletWebRequest request) {
return this.errorAttributes.getErrorAttributes(request, getErrorAttributeOptions(request));
}
private ErrorAttributeOptions getErrorAttributeOptions(ServletWebRequest request) {
ErrorAttributeOptions options = ErrorAttributeOptions.defaults();
if (this.errorProperties.isIncludeException()) {
options = options.including(Include.EXCEPTION);
}
if (includeStackTrace(request)) {
options = options.including(Include.STACK_TRACE);
}
if (includeMessage(request)) {
options = options.including(Include.MESSAGE);
}
if (includeBindingErrors(request)) {
options = options.including(Include.BINDING_ERRORS);
}
options = includePath(request) ? options.including(Include.PATH) : options.excluding(Include.PATH);
return options;
}
private boolean includeStackTrace(ServletWebRequest request) {
return switch (this.errorProperties.getIncludeStacktrace()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "trace");
case NEVER -> false;
};
}
private boolean includeMessage(ServletWebRequest request) {
return switch (this.errorProperties.getIncludeMessage()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "message");
case NEVER -> false;
};
}
private boolean includeBindingErrors(ServletWebRequest request) {
return switch (this.errorProperties.getIncludeBindingErrors()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "errors");
case NEVER -> false;
};
}
private boolean includePath(ServletWebRequest request) {
return switch (this.errorProperties.getIncludePath()) {
case ALWAYS -> true;
case ON_PARAM -> getBooleanParameter(request, "path");
case NEVER -> false;
};
}
protected boolean getBooleanParameter(ServletWebRequest request, String parameterName) {
String parameter = request.getParameter(parameterName);
if (parameter == null) {
return false;
}
return !"false".equalsIgnoreCase(parameter);
}
}

View File

@@ -0,0 +1,136 @@
/*
* 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.springframework.beans.factory.ListableBeanFactory;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.ManagementContextType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.web.error.ErrorPage;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.boot.web.server.servlet.ConfigurableServletWebServerFactory;
import org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletRegistrationBean;
import org.springframework.boot.webmvc.error.ErrorAttributes;
import org.springframework.context.annotation.Bean;
import org.springframework.core.Ordered;
import org.springframework.web.context.request.RequestContextListener;
import org.springframework.web.filter.RequestContextFilter;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Spring MVC
* infrastructure when a separate management context with a web server running on a
* different port is required.
*
* @author Stephane Nicoll
* @author Andy Wilkinson
* @author Phillip Webb
*/
@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@EnableWebMvc
class WebMvcEndpointChildContextConfiguration {
/*
* The error controller is present but not mapped as an endpoint in this context
* because of the DispatcherServlet having had its HandlerMapping explicitly disabled.
* So we expose the same feature but only for machine endpoints.
*/
@Bean
@ConditionalOnBean(ErrorAttributes.class)
ManagementErrorEndpoint errorEndpoint(ErrorAttributes errorAttributes, ServerProperties serverProperties) {
return new ManagementErrorEndpoint(errorAttributes, serverProperties.getError());
}
@Bean
@ConditionalOnBean(ErrorAttributes.class)
ManagementErrorPageCustomizer managementErrorPageCustomizer(ServerProperties serverProperties) {
return new ManagementErrorPageCustomizer(serverProperties);
}
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
// Ensure the parent configuration does not leak down to us
dispatcherServlet.setDetectAllHandlerAdapters(false);
dispatcherServlet.setDetectAllHandlerExceptionResolvers(false);
dispatcherServlet.setDetectAllHandlerMappings(false);
dispatcherServlet.setDetectAllViewResolvers(false);
return dispatcherServlet;
}
@Bean(name = DispatcherServletAutoConfiguration.DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
DispatcherServletRegistrationBean dispatcherServletRegistrationBean(DispatcherServlet dispatcherServlet) {
return new DispatcherServletRegistrationBean(dispatcherServlet, "/");
}
@Bean(name = DispatcherServlet.HANDLER_MAPPING_BEAN_NAME)
CompositeHandlerMapping compositeHandlerMapping() {
return new CompositeHandlerMapping();
}
@Bean(name = DispatcherServlet.HANDLER_ADAPTER_BEAN_NAME)
CompositeHandlerAdapter compositeHandlerAdapter(ListableBeanFactory beanFactory) {
return new CompositeHandlerAdapter(beanFactory);
}
@Bean(name = DispatcherServlet.HANDLER_EXCEPTION_RESOLVER_BEAN_NAME)
CompositeHandlerExceptionResolver compositeHandlerExceptionResolver() {
return new CompositeHandlerExceptionResolver();
}
@Bean
@ConditionalOnMissingBean({ RequestContextListener.class, RequestContextFilter.class })
RequestContextFilter requestContextFilter() {
return new OrderedRequestContextFilter();
}
/**
* {@link WebServerFactoryCustomizer} to add an {@link ErrorPage} so that the
* {@link ManagementErrorEndpoint} can be used.
*/
static class ManagementErrorPageCustomizer
implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
private final ServerProperties properties;
ManagementErrorPageCustomizer(ServerProperties properties) {
this.properties = properties;
}
@Override
public void customize(ConfigurableServletWebServerFactory factory) {
factory.addErrorPages(new ErrorPage(this.properties.getError().getPath()));
}
@Override
public int getOrder() {
return 10; // Run after ManagementWebServerFactoryCustomizer
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Actuator Spring MVC support.
*/
package org.springframework.boot.webmvc.actuate.autoconfigure.web;

View File

@@ -0,0 +1,513 @@
/*
* 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.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import reactor.core.publisher.Flux;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.ProducibleOperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.SecurityContext;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.endpoint.web.WebServerNamespace;
import org.springframework.boot.web.server.context.WebServerApplicationContext;
import org.springframework.boot.webmvc.actuate.endpoint.web.AbstractWebMvcEndpointHandlerMapping.AbstractWebMvcEndpointHandlerMappingRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
/**
* A custom {@link HandlerMapping} that makes {@link ExposableWebEndpoint web endpoints}
* available over HTTP using Spring MVC.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Phillip Webb
* @author Brian Clozel
* @since 2.0.0
*/
@ImportRuntimeHints(AbstractWebMvcEndpointHandlerMappingRuntimeHints.class)
public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappingInfoHandlerMapping
implements InitializingBean {
private final EndpointMapping endpointMapping;
private final Collection<ExposableWebEndpoint> endpoints;
private final EndpointMediaTypes endpointMediaTypes;
private final CorsConfiguration corsConfiguration;
private final boolean shouldRegisterLinksMapping;
private final Method handleMethod = ReflectionUtils.findMethod(OperationHandler.class, "handle",
HttpServletRequest.class, Map.class);
private RequestMappingInfo.BuilderConfiguration builderConfig = new RequestMappingInfo.BuilderConfiguration();
/**
* Creates a new {@code WebEndpointHandlerMapping} that provides mappings for the
* operations of the given {@code webEndpoints}.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
*/
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
boolean shouldRegisterLinksMapping) {
this(endpointMapping, endpoints, endpointMediaTypes, null, shouldRegisterLinksMapping);
}
/**
* Creates a new {@code AbstractWebMvcEndpointHandlerMapping} that provides mappings
* for the operations of the given endpoints.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
*/
public AbstractWebMvcEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableWebEndpoint> endpoints, EndpointMediaTypes endpointMediaTypes,
CorsConfiguration corsConfiguration, boolean shouldRegisterLinksMapping) {
this.endpointMapping = endpointMapping;
this.endpoints = endpoints;
this.endpointMediaTypes = endpointMediaTypes;
this.corsConfiguration = corsConfiguration;
this.shouldRegisterLinksMapping = shouldRegisterLinksMapping;
setOrder(-100);
}
@Override
public void afterPropertiesSet() {
this.builderConfig = new RequestMappingInfo.BuilderConfiguration();
this.builderConfig.setPatternParser(getPatternParser());
super.afterPropertiesSet();
}
@Override
protected void initHandlerMethods() {
for (ExposableWebEndpoint endpoint : this.endpoints) {
for (WebOperation operation : endpoint.getOperations()) {
registerMappingForOperation(endpoint, operation);
}
}
if (this.shouldRegisterLinksMapping) {
registerLinksMapping();
}
}
@Override
protected HandlerMethod createHandlerMethod(Object handler, Method method) {
HandlerMethod handlerMethod = super.createHandlerMethod(handler, method);
return new WebMvcEndpointHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
}
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String path = predicate.getPath();
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
path = path.replace("{*" + matchAllRemainingPathSegmentsVariable + "}", "**");
}
registerMapping(endpoint, predicate, operation, path);
}
protected void registerMapping(ExposableWebEndpoint endpoint, WebOperationRequestPredicate predicate,
WebOperation operation, String path) {
ServletWebOperation servletWebOperation = wrapServletWebOperation(endpoint, operation,
new ServletWebOperationAdapter(operation));
registerMapping(createRequestMappingInfo(predicate, path), new OperationHandler(servletWebOperation),
this.handleMethod);
}
/**
* Hook point that allows subclasses to wrap the {@link ServletWebOperation} before
* it's called. Allows additional features, such as security, to be added.
* @param endpoint the source endpoint
* @param operation the source operation
* @param servletWebOperation the servlet web operation to wrap
* @return a wrapped servlet web operation
*/
protected ServletWebOperation wrapServletWebOperation(ExposableWebEndpoint endpoint, WebOperation operation,
ServletWebOperation servletWebOperation) {
return servletWebOperation;
}
private RequestMappingInfo createRequestMappingInfo(WebOperationRequestPredicate predicate, String path) {
String subPath = this.endpointMapping.createSubPath(path);
List<String> paths = new ArrayList<>();
paths.add(subPath);
if (!StringUtils.hasLength(subPath)) {
paths.add("/");
}
return RequestMappingInfo.paths(paths.toArray(new String[0]))
.options(this.builderConfig)
.methods(RequestMethod.valueOf(predicate.getHttpMethod().name()))
.consumes(predicate.getConsumes().toArray(new String[0]))
.produces(predicate.getProduces().toArray(new String[0]))
.build();
}
private void registerLinksMapping() {
String path = this.endpointMapping.getPath();
String linksPath = (StringUtils.hasLength(path)) ? this.endpointMapping.createSubPath("/") : "/";
RequestMappingInfo mapping = RequestMappingInfo.paths(linksPath)
.methods(RequestMethod.GET)
.produces(this.endpointMediaTypes.getProduced().toArray(new String[0]))
.options(this.builderConfig)
.build();
LinksHandler linksHandler = getLinksHandler();
registerMapping(mapping, linksHandler, ReflectionUtils.findMethod(linksHandler.getClass(), "links",
HttpServletRequest.class, HttpServletResponse.class));
}
@Override
protected boolean hasCorsConfigurationSource(Object handler) {
return this.corsConfiguration != null;
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) {
return this.corsConfiguration;
}
@Override
protected CorsConfiguration getCorsConfiguration(Object handler, HttpServletRequest request) {
CorsConfiguration corsConfiguration = super.getCorsConfiguration(handler, request);
return (corsConfiguration != null) ? corsConfiguration : this.corsConfiguration;
}
@Override
protected boolean isHandler(Class<?> beanType) {
return false;
}
@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
return null;
}
/**
* Return the Handler providing actuator links at the root endpoint.
* @return the links handler
*/
protected abstract LinksHandler getLinksHandler();
/**
* Return the web endpoints being mapped.
* @return the endpoints
*/
public Collection<ExposableWebEndpoint> getEndpoints() {
return this.endpoints;
}
/**
* Handler providing actuator links at the root endpoint.
*/
@FunctionalInterface
protected interface LinksHandler {
Object links(HttpServletRequest request, HttpServletResponse response);
}
/**
* A servlet web operation that can be handled by Spring MVC.
*/
@FunctionalInterface
protected interface ServletWebOperation {
Object handle(HttpServletRequest request, Map<String, String> body);
}
/**
* Adapter class to convert an {@link OperationInvoker} into a
* {@link ServletWebOperation}.
*/
private static class ServletWebOperationAdapter implements ServletWebOperation {
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
private static final List<Function<Object, Object>> BODY_CONVERTERS;
static {
List<Function<Object, Object>> converters = new ArrayList<>();
if (ClassUtils.isPresent("reactor.core.publisher.Flux",
ServletWebOperationAdapter.class.getClassLoader())) {
converters.add(new FluxBodyConverter());
}
BODY_CONVERTERS = Collections.unmodifiableList(converters);
}
private final WebOperation operation;
ServletWebOperationAdapter(WebOperation operation) {
this.operation = operation;
}
@Override
public Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
Map<String, Object> arguments = getArguments(request, body);
try {
ServletSecurityContext securityContext = new ServletSecurityContext(request);
ProducibleOperationArgumentResolver producibleOperationArgumentResolver = new ProducibleOperationArgumentResolver(
() -> headers.get("Accept"));
InvocationContext invocationContext = new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver(request), producibleOperationArgumentResolver);
return handleResult(this.operation.invoke(invocationContext), HttpMethod.valueOf(request.getMethod()));
}
catch (InvalidEndpointRequestException ex) {
throw new InvalidEndpointBadRequestException(ex);
}
}
private OperationArgumentResolver serverNamespaceArgumentResolver(HttpServletRequest request) {
if (ClassUtils.isPresent("org.springframework.boot.web.server.context.WebServerApplicationContext", null)) {
return OperationArgumentResolver.of(WebServerNamespace.class, () -> {
WebApplicationContext applicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
return WebServerNamespace.from(WebServerApplicationContext.getServerNamespace(applicationContext));
});
}
return OperationArgumentResolver.of(WebServerNamespace.class, () -> null);
}
@Override
public String toString() {
return "Actuator web endpoint '" + this.operation.getId() + "'";
}
private Map<String, Object> getArguments(HttpServletRequest request, Map<String, String> body) {
Map<String, Object> arguments = new LinkedHashMap<>(getTemplateVariables(request));
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
arguments.put(matchAllRemainingPathSegmentsVariable, getRemainingPathSegments(request));
}
if (body != null && HttpMethod.POST.name().equals(request.getMethod())) {
arguments.putAll(body);
}
request.getParameterMap()
.forEach((name, values) -> arguments.put(name,
(values.length != 1) ? Arrays.asList(values) : values[0]));
return arguments;
}
private Object getRemainingPathSegments(HttpServletRequest request) {
String[] pathTokens = tokenize(request, HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, true);
String[] patternTokens = tokenize(request, HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, false);
int numberOfRemainingPathSegments = pathTokens.length - patternTokens.length + 1;
Assert.state(numberOfRemainingPathSegments >= 0, "Unable to extract remaining path segments");
String[] remainingPathSegments = new String[numberOfRemainingPathSegments];
System.arraycopy(pathTokens, patternTokens.length - 1, remainingPathSegments, 0,
numberOfRemainingPathSegments);
return remainingPathSegments;
}
private String[] tokenize(HttpServletRequest request, String attributeName, boolean decode) {
String value = (String) request.getAttribute(attributeName);
String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true);
if (decode) {
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
}
return segments;
}
@SuppressWarnings("unchecked")
private Map<String, String> getTemplateVariables(HttpServletRequest request) {
return (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private Object handleResult(Object result, HttpMethod httpMethod) {
if (result == null) {
return new ResponseEntity<>(
(httpMethod != HttpMethod.GET) ? HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND);
}
if (!(result instanceof WebEndpointResponse<?> response)) {
return convertIfNecessary(result);
}
MediaType contentType = (response.getContentType() != null) ? new MediaType(response.getContentType())
: null;
return ResponseEntity.status(response.getStatus())
.contentType(contentType)
.body(convertIfNecessary(response.getBody()));
}
private Object convertIfNecessary(Object body) {
for (Function<Object, Object> converter : BODY_CONVERTERS) {
body = converter.apply(body);
}
return body;
}
private static final class FluxBodyConverter implements Function<Object, Object> {
@Override
public Object apply(Object body) {
if (!(body instanceof Flux)) {
return body;
}
return ((Flux<?>) body).collectList();
}
}
}
/**
* Handler for a {@link ServletWebOperation}.
*/
private static final class OperationHandler {
private final ServletWebOperation operation;
OperationHandler(ServletWebOperation operation) {
this.operation = operation;
}
@ResponseBody
@Reflective
Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
return this.operation.handle(request, body);
}
@Override
public String toString() {
return this.operation.toString();
}
}
/**
* {@link HandlerMethod} subclass for endpoint information logging.
*/
private static class WebMvcEndpointHandlerMethod extends HandlerMethod {
WebMvcEndpointHandlerMethod(Object bean, Method method) {
super(bean, method);
}
@Override
public String toString() {
return getBean().toString();
}
@Override
public HandlerMethod createWithResolvedBean() {
return this;
}
}
/**
* Nested exception used to wrap an {@link InvalidEndpointRequestException} and
* provide a {@link HttpStatus#BAD_REQUEST} status.
*/
private static class InvalidEndpointBadRequestException extends ResponseStatusException {
InvalidEndpointBadRequestException(InvalidEndpointRequestException cause) {
super(HttpStatus.BAD_REQUEST, cause.getReason(), cause);
}
}
private static final class ServletSecurityContext implements SecurityContext {
private final HttpServletRequest request;
private ServletSecurityContext(HttpServletRequest request) {
this.request = request;
}
@Override
public Principal getPrincipal() {
return this.request.getUserPrincipal();
}
@Override
public boolean isUserInRole(String role) {
return this.request.isUserInRole(role);
}
}
static class AbstractWebMvcEndpointHandlerMappingRuntimeHints implements RuntimeHintsRegistrar {
private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, OperationHandler.class);
}
}
}

View File

@@ -0,0 +1,79 @@
/*
* 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.Collection;
import java.util.Collections;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.boot.actuate.health.AdditionalHealthEndpointPath;
import org.springframework.boot.actuate.health.HealthEndpointGroup;
import org.springframework.web.servlet.HandlerMapping;
/**
* A custom {@link HandlerMapping} that allows health groups to be mapped to an additional
* path.
*
* @author Madhura Bhave
* @since 2.6.0
*/
public class AdditionalHealthEndpointPathsWebMvcHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final ExposableWebEndpoint healthEndpoint;
private final Set<HealthEndpointGroup> groups;
public AdditionalHealthEndpointPathsWebMvcHandlerMapping(ExposableWebEndpoint healthEndpoint,
Set<HealthEndpointGroup> groups) {
super(new EndpointMapping(""), asList(healthEndpoint), null, false);
this.healthEndpoint = healthEndpoint;
this.groups = groups;
}
private static Collection<ExposableWebEndpoint> asList(ExposableWebEndpoint healthEndpoint) {
return (healthEndpoint != null) ? Collections.singletonList(healthEndpoint) : Collections.emptyList();
}
@Override
protected void initHandlerMethods() {
if (this.healthEndpoint == null) {
return;
}
for (WebOperation operation : this.healthEndpoint.getOperations()) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String matchAllRemainingPathSegmentsVariable = predicate.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
for (HealthEndpointGroup group : this.groups) {
AdditionalHealthEndpointPath additionalPath = group.getAdditionalPath();
if (additionalPath != null) {
registerMapping(this.healthEndpoint, predicate, operation, additionalPath.getValue());
}
}
}
}
}
@Override
protected LinksHandler getLinksHandler() {
return null;
}
}

View File

@@ -0,0 +1,165 @@
/*
* 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.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import org.springframework.boot.actuate.endpoint.Access;
import org.springframework.boot.actuate.endpoint.EndpointAccessResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
/**
* {@link HandlerMapping} that exposes
* {@link org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint @ControllerEndpoint}
* and
* {@link org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint @RestControllerEndpoint}
* annotated endpoints over Spring MVC.
*
* @author Phillip Webb
* @since 2.0.0
* @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support
*/
@Deprecated(since = "3.3.5", forRemoval = true)
@SuppressWarnings("removal")
public class ControllerEndpointHandlerMapping extends RequestMappingHandlerMapping {
private static final Set<RequestMethod> READ_ONLY_ACCESS_REQUEST_METHODS = EnumSet.of(RequestMethod.GET,
RequestMethod.HEAD);
private final EndpointMapping endpointMapping;
private final CorsConfiguration corsConfiguration;
private final Map<Object, ExposableControllerEndpoint> handlers;
private final EndpointAccessResolver accessResolver;
/**
* 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
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration) {
this(endpointMapping, endpoints, corsConfiguration, (endpointId, defaultAccess) -> Access.NONE);
}
/**
* 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
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
* @param endpointAccessResolver resolver for endpoint access
*/
public ControllerEndpointHandlerMapping(EndpointMapping endpointMapping,
Collection<ExposableControllerEndpoint> endpoints, CorsConfiguration corsConfiguration,
EndpointAccessResolver endpointAccessResolver) {
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;
this.accessResolver = endpointAccessResolver;
setOrder(-100);
}
private Map<Object, ExposableControllerEndpoint> getHandlers(Collection<ExposableControllerEndpoint> endpoints) {
Map<Object, ExposableControllerEndpoint> handlers = new LinkedHashMap<>();
endpoints.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);
Access access = this.accessResolver.accessFor(endpoint.getEndpointId(), endpoint.getDefaultAccess());
if (access == Access.NONE) {
return;
}
if (access == Access.READ_ONLY) {
mapping = withReadOnlyAccess(access, mapping);
if (CollectionUtils.isEmpty(mapping.getMethodsCondition().getMethods())) {
return;
}
}
mapping = withEndpointMappedPatterns(endpoint, mapping);
super.registerHandlerMethod(handler, method, mapping);
}
private RequestMappingInfo withReadOnlyAccess(Access access, RequestMappingInfo mapping) {
Set<RequestMethod> methods = mapping.getMethodsCondition().getMethods();
Set<RequestMethod> modifiedMethods = new HashSet<>(methods);
if (modifiedMethods.isEmpty()) {
modifiedMethods.addAll(READ_ONLY_ACCESS_REQUEST_METHODS);
}
else {
modifiedMethods.retainAll(READ_ONLY_ACCESS_REQUEST_METHODS);
}
return mapping.mutate().methods(modifiedMethods.toArray(new RequestMethod[0])).build();
}
private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoint endpoint,
RequestMappingInfo mapping) {
Set<PathPattern> patterns = mapping.getPathPatternsCondition().getPatterns();
if (patterns.isEmpty()) {
patterns = Collections.singleton(getPatternParser().parse(""));
}
String[] endpointMappedPatterns = patterns.stream()
.map((pattern) -> getEndpointMappedPattern(endpoint, pattern))
.toArray(String[]::new);
return mapping.mutate().paths(endpointMappedPatterns).build();
}
private String getEndpointMappedPattern(ExposableControllerEndpoint endpoint, PathPattern pattern) {
return this.endpointMapping.createSubPath(endpoint.getRootPath() + pattern);
}
@Override
protected boolean hasCorsConfigurationSource(Object handler) {
return this.corsConfiguration != null;
}
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mapping) {
return this.corsConfiguration;
}
}

View File

@@ -0,0 +1,114 @@
/*
* 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.Collection;
import java.util.Collections;
import java.util.Map;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.aot.hint.BindingReflectionHintsRegistrar;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar;
import org.springframework.boot.actuate.endpoint.OperationResponseBody;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
import org.springframework.boot.actuate.endpoint.web.Link;
import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping.WebMvcEndpointHandlerMappingRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.servlet.HandlerMapping;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring MVC.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
@ImportRuntimeHints(WebMvcEndpointHandlerMappingRuntimeHints.class)
public class WebMvcEndpointHandlerMapping extends AbstractWebMvcEndpointHandlerMapping {
private final EndpointLinksResolver linksResolver;
/**
* Creates a new {@code WebMvcEndpointHandlerMapping} instance that provides mappings
* for the given endpoints.
* @param endpointMapping the base mapping for all endpoints
* @param endpoints the web endpoints
* @param endpointMediaTypes media types consumed and produced by the endpoints
* @param corsConfiguration the CORS configuration for the endpoints or {@code null}
* @param linksResolver resolver for determining links to available endpoints
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
*/
public WebMvcEndpointHandlerMapping(EndpointMapping endpointMapping, Collection<ExposableWebEndpoint> endpoints,
EndpointMediaTypes endpointMediaTypes, CorsConfiguration corsConfiguration,
EndpointLinksResolver linksResolver, boolean shouldRegisterLinksMapping) {
super(endpointMapping, endpoints, endpointMediaTypes, corsConfiguration, shouldRegisterLinksMapping);
this.linksResolver = linksResolver;
setOrder(-100);
}
@Override
protected LinksHandler getLinksHandler() {
return new WebMvcLinksHandler();
}
/**
* Handler for root endpoint providing links.
*/
class WebMvcLinksHandler implements LinksHandler {
@Override
@ResponseBody
@Reflective
public Map<String, Map<String, Link>> links(HttpServletRequest request, HttpServletResponse response) {
Map<String, Link> links = WebMvcEndpointHandlerMapping.this.linksResolver
.resolveLinks(request.getRequestURL().toString());
return OperationResponseBody.of(Collections.singletonMap("_links", links));
}
@Override
public String toString() {
return "Actuator root web endpoint";
}
}
static class WebMvcEndpointHandlerMappingRuntimeHints implements RuntimeHintsRegistrar {
private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar();
private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, WebMvcLinksHandler.class);
this.bindingRegistrar.registerReflectionHints(hints.reflection(), Link.class);
}
}
}

View File

@@ -0,0 +1,20 @@
/*
* 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.
*/
/**
* Spring MVC support for actuator endpoints.
*/
package org.springframework.boot.webmvc.actuate.endpoint.web;

View File

@@ -1 +1,2 @@
org.springframework.boot.webmvc.actuate.autoconfigure.endpoint.web.WebMvcEndpointManagementContextConfiguration
org.springframework.boot.webmvc.actuate.autoconfigure.web.WebMvcEndpointChildContextConfiguration

View File

@@ -1,3 +1,4 @@
org.springframework.boot.webmvc.actuate.autoconfigure.health.WebMvcHealthEndpointExtensionAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.error.ErrorMvcAutoConfiguration

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

View File

@@ -23,11 +23,11 @@ import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.http.converter.autoconfigure.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory;
import org.springframework.boot.webmvc.actuate.endpoint.web.WebMvcEndpointHandlerMapping;
import org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration;
import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration;
import org.springframework.context.ApplicationContext;