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