Move Actuator infrastructure for WebMvc to spring-boot-webmvc
This commit is contained in:
committed by
Phillip Webb
parent
272eca17e5
commit
46a5ea0ee7
@@ -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));
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -1 +1,2 @@
|
||||
org.springframework.boot.webmvc.actuate.autoconfigure.endpoint.web.WebMvcEndpointManagementContextConfiguration
|
||||
org.springframework.boot.webmvc.actuate.autoconfigure.web.WebMvcEndpointChildContextConfiguration
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user