Move Actuator infrastructure for WebFlux to spring-boot-webflux

This commit is contained in:
Stéphane Nicoll
2025-05-30 12:06:09 +02:00
committed by Phillip Webb
parent b9aa01c1c4
commit e0a6237b8a
41 changed files with 250 additions and 116 deletions

View File

@@ -16,11 +16,13 @@ dependencies {
implementation(project(":spring-boot-project:spring-boot-http-codec"))
implementation(project(":spring-boot-project:spring-boot-web-server"))
optional(project(":spring-boot-project:spring-boot-actuator"))
optional(project(":spring-boot-project:spring-boot-actuator-autoconfigure"))
optional(project(":spring-boot-project:spring-boot-autoconfigure"))
optional(project(":spring-boot-project:spring-boot-metrics"))
optional(project(":spring-boot-project:spring-boot-observation"))
optional(project(":spring-boot-project:spring-boot-validation"))
optional("com.fasterxml.jackson.core:jackson-databind")
optional("org.springframework.security:spring-security-core")
testFixturesApi(testFixtures(project(":spring-boot-project:spring-boot-actuator")))
testFixturesImplementation(project(":spring-boot-project:spring-boot-jackson"))
@@ -29,8 +31,11 @@ dependencies {
testImplementation(project(":spring-boot-project:spring-boot-mustache"))
testImplementation(project(":spring-boot-project:spring-boot-reactor-netty"))
testImplementation(project(":spring-boot-project:spring-boot-test"))
testImplementation(project(":spring-boot-project:spring-boot-tomcat"))
testImplementation(project(":spring-boot-project:spring-boot-tools:spring-boot-test-support"))
testImplementation(testFixtures(project(":spring-boot-project:spring-boot-actuator-autoconfigure")))
testImplementation(testFixtures(project(":spring-boot-project:spring-boot-web-server")))
testImplementation("io.projectreactor:reactor-test")
testImplementation("org.aspectj:aspectjweaver")
testRuntimeOnly("ch.qos.logback:logback-classic")

View File

@@ -0,0 +1,195 @@
/*
* 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.webflux.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 java.util.function.Supplier;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanPostProcessor;
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.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.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.context.properties.EnableConfigurationProperties;
import org.springframework.boot.webflux.actuate.endpoint.web.AdditionalHealthEndpointPathsWebFluxHandlerMapping;
import org.springframework.boot.webflux.actuate.endpoint.web.WebFluxEndpointHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Role;
import org.springframework.core.codec.Encoder;
import org.springframework.core.env.Environment;
import org.springframework.http.MediaType;
import org.springframework.http.codec.EncoderHttpMessageWriter;
import org.springframework.http.codec.HttpMessageWriter;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
import org.springframework.util.function.SingletonSupplier;
import org.springframework.web.reactive.DispatcherHandler;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for Reactive
* {@link Endpoint @Endpoint} concerns.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0
*/
@ManagementContextConfiguration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass({ DispatcherHandler.class, HttpHandler.class })
@ConditionalOnBean(WebEndpointsSupplier.class)
@EnableConfigurationProperties(CorsEndpointProperties.class)
public class WebFluxEndpointManagementContextConfiguration {
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
public WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpointsSupplier controllerEndpointsSupplier,
EndpointMediaTypes endpointMediaTypes, CorsEndpointProperties corsProperties,
WebEndpointProperties webEndpointProperties, Environment environment) {
String basePath = webEndpointProperties.getBasePath();
EndpointMapping endpointMapping = new EndpointMapping(basePath);
Collection<ExposableWebEndpoint> endpoints = webEndpointsSupplier.getEndpoints();
List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
allEndpoints.addAll(endpoints);
allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
return new WebFluxEndpointHandlerMapping(endpointMapping, endpoints, endpointMediaTypes,
corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
shouldRegisterLinksMapping(webEndpointProperties, environment, basePath));
}
private boolean shouldRegisterLinksMapping(WebEndpointProperties properties, Environment environment,
String basePath) {
return properties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath)
|| ManagementPortType.get(environment) == ManagementPortType.DIFFERENT);
}
@Bean
@ConditionalOnManagementPort(ManagementPortType.DIFFERENT)
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
@ConditionalOnBean(HealthEndpoint.class)
public AdditionalHealthEndpointPathsWebFluxHandlerMapping managementHealthEndpointWebFluxHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint healthEndpoint = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
.findFirst()
.orElse(null);
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), healthEndpoint,
groups.getAllWithAdditionalPath(WebServerNamespace.MANAGEMENT));
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("removal")
@Deprecated(since = "3.3.5", forRemoval = true)
public org.springframework.boot.webflux.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.webflux.actuate.endpoint.web.ControllerEndpointHandlerMapping(
endpointMapping, controllerEndpointsSupplier.getEndpoints(), corsProperties.toCorsConfiguration(),
endpointAccessResolver);
}
@Bean
@ConditionalOnBean(EndpointObjectMapper.class)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor serverCodecConfigurerEndpointObjectMapperBeanPostProcessor(
ObjectProvider<EndpointObjectMapper> endpointObjectMapper) {
return new ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(
SingletonSupplier.of(endpointObjectMapper::getObject));
}
/**
* {@link BeanPostProcessor} to apply {@link EndpointObjectMapper} for
* {@link OperationResponseBody} to
* {@link org.springframework.http.codec.json.Jackson2JsonEncoder} instances.
*/
static class ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor implements BeanPostProcessor {
private static final List<MediaType> MEDIA_TYPES = Collections
.unmodifiableList(Arrays.asList(MediaType.APPLICATION_JSON, new MediaType("application", "*+json")));
private final Supplier<EndpointObjectMapper> endpointObjectMapper;
ServerCodecConfigurerEndpointObjectMapperBeanPostProcessor(
Supplier<EndpointObjectMapper> endpointObjectMapper) {
this.endpointObjectMapper = endpointObjectMapper;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof ServerCodecConfigurer serverCodecConfigurer) {
process(serverCodecConfigurer);
}
return bean;
}
private void process(ServerCodecConfigurer configurer) {
for (HttpMessageWriter<?> writer : configurer.getWriters()) {
if (writer instanceof EncoderHttpMessageWriter<?> encoderHttpMessageWriter) {
process((encoderHttpMessageWriter).getEncoder());
}
}
}
@SuppressWarnings({ "removal", "deprecation" })
private void process(Encoder<?> encoder) {
if (encoder instanceof org.springframework.http.codec.json.Jackson2JsonEncoder jackson2JsonEncoder) {
jackson2JsonEncoder.registerObjectMappersForType(OperationResponseBody.class, (associations) -> {
ObjectMapper objectMapper = this.endpointObjectMapper.get().get();
MEDIA_TYPES.forEach((mimeType) -> associations.put(mimeType, objectMapper));
});
}
}
}
}

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 exposing actuator web endpoints using WebFlux.
*/
package org.springframework.boot.webflux.actuate.autoconfigure.endpoint.web;

View File

@@ -0,0 +1,62 @@
/*
* 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.webflux.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.EndpointMapping;
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.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.webflux.actuate.endpoint.web.AdditionalHealthEndpointPathsWebFluxHandlerMapping;
import org.springframework.context.annotation.Bean;
/**
* {@link EnableAutoConfiguration Auto-configuration} for {@link HealthEndpoint} web
* extension with Spring WebFlux.
*
* @author Stephane Nicoll
* @since 4.0.0
*/
@AutoConfiguration
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass(HealthEndpoint.class)
@ConditionalOnAvailableEndpoint(endpoint = HealthEndpoint.class, exposure = EndpointExposure.WEB)
public class WebFluxHealthEndpointExtensionAutoConfiguration {
@Bean
AdditionalHealthEndpointPathsWebFluxHandlerMapping healthEndpointWebFluxHandlerMapping(
WebEndpointsSupplier webEndpointsSupplier, HealthEndpointGroups groups) {
Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
ExposableWebEndpoint health = webEndpoints.stream()
.filter((endpoint) -> endpoint.getEndpointId().equals(HealthEndpoint.ID))
.findFirst()
.orElse(null);
return new AdditionalHealthEndpointPathsWebFluxHandlerMapping(new EndpointMapping(""), health,
groups.getAllWithAdditionalPath(WebServerNamespace.SERVER));
}
}

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 using Spring WebFlux.
*/
package org.springframework.boot.webflux.actuate.autoconfigure.health;

View File

@@ -0,0 +1,71 @@
/*
* 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.webflux.actuate.autoconfigure.web;
import java.util.Collections;
import java.util.Map;
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.actuate.autoconfigure.web.server.ManagementServerProperties;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementWebServerFactoryCustomizer;
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.web.server.ConfigurableWebServerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.http.server.reactive.ContextPathCompositeHandler;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.util.StringUtils;
import org.springframework.web.reactive.DispatcherHandler;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
/**
* {@link ManagementContextConfiguration @ManagementContextConfiguration} for reactive web
* infrastructure when a separate management context with a web server running on a
* different port is required.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Moritz Halbritter
*/
@ManagementContextConfiguration(value = ManagementContextType.CHILD, proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.REACTIVE)
@ConditionalOnClass(DispatcherHandler.class)
@EnableWebFlux
class WebFluxManagementChildContextConfiguration {
@Bean
ManagementWebServerFactoryCustomizer<ConfigurableWebServerFactory> reactiveManagementWebServerFactoryCustomizer(
ListableBeanFactory beanFactory) {
return new ManagementWebServerFactoryCustomizer<>(beanFactory);
}
@Bean
HttpHandler httpHandler(ApplicationContext applicationContext, ManagementServerProperties properties) {
HttpHandler httpHandler = WebHttpHandlerBuilder.applicationContext(applicationContext).build();
if (StringUtils.hasText(properties.getBasePath())) {
Map<String, HttpHandler> handlersMap = Collections.singletonMap(properties.getBasePath(), httpHandler);
return new ContextPathCompositeHandler(handlersMap);
}
return httpHandler;
}
}

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.
*/
/**
* Configuration for a WebFlux-based management context.
*/
package org.springframework.boot.webflux.actuate.autoconfigure.web;

View File

@@ -0,0 +1,552 @@
/*
* 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.webflux.actuate.endpoint.web;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
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.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationArgumentResolver;
import org.springframework.boot.actuate.endpoint.OperationType;
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.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping.AbstractWebFluxEndpointHandlerMappingRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authorization.AuthorityAuthorizationManager;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.util.AntPathMatcher;
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.cors.CorsConfiguration;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.pattern.PathPattern;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring WebFlux.
*
* @author Andy Wilkinson
* @author Madhura Bhave
* @author Phillip Webb
* @author Brian Clozel
* @author Scott Frederick
* @since 4.0.0
*/
@ImportRuntimeHints(AbstractWebFluxEndpointHandlerMappingRuntimeHints.class)
public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappingInfoHandlerMapping {
private final EndpointMapping endpointMapping;
private final Collection<ExposableWebEndpoint> endpoints;
private final EndpointMediaTypes endpointMediaTypes;
private final CorsConfiguration corsConfiguration;
private final Method handleWriteMethod = ReflectionUtils.findMethod(WriteOperationHandler.class, "handle",
ServerWebExchange.class, Map.class);
private final Method handleReadMethod = ReflectionUtils.findMethod(ReadOperationHandler.class, "handle",
ServerWebExchange.class);
private final boolean shouldRegisterLinksMapping;
/**
* Creates a new {@code AbstractWebFluxEndpointHandlerMapping} 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 corsConfiguration the CORS configuration for the endpoints
* @param shouldRegisterLinksMapping whether the links endpoint should be registered
*/
public AbstractWebFluxEndpointHandlerMapping(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
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 WebFluxEndpointHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
}
private void registerMappingForOperation(ExposableWebEndpoint endpoint, WebOperation operation) {
RequestMappingInfo requestMappingInfo = createRequestMappingInfo(operation);
if (operation.getType() == OperationType.WRITE) {
ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, operation,
new ReactiveWebOperationAdapter(operation));
registerMapping(requestMappingInfo, new WriteOperationHandler((reactiveWebOperation)),
this.handleWriteMethod);
}
else {
registerReadMapping(requestMappingInfo, endpoint, operation);
}
}
protected void registerReadMapping(RequestMappingInfo requestMappingInfo, ExposableWebEndpoint endpoint,
WebOperation operation) {
ReactiveWebOperation reactiveWebOperation = wrapReactiveWebOperation(endpoint, operation,
new ReactiveWebOperationAdapter(operation));
registerMapping(requestMappingInfo, new ReadOperationHandler((reactiveWebOperation)), this.handleReadMethod);
}
/**
* Hook point that allows subclasses to wrap the {@link ReactiveWebOperation} before
* it's called. Allows additional features, such as security, to be added.
* @param endpoint the source endpoint
* @param operation the source operation
* @param reactiveWebOperation the reactive web operation to wrap
* @return a wrapped reactive web operation
*/
protected ReactiveWebOperation wrapReactiveWebOperation(ExposableWebEndpoint endpoint, WebOperation operation,
ReactiveWebOperation reactiveWebOperation) {
return reactiveWebOperation;
}
private RequestMappingInfo createRequestMappingInfo(WebOperation operation) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String path = this.endpointMapping.createSubPath(predicate.getPath());
List<String> paths = new ArrayList<>();
paths.add(path);
if (!StringUtils.hasText(path)) {
paths.add("/");
}
RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name());
String[] consumes = StringUtils.toStringArray(predicate.getConsumes());
String[] produces = StringUtils.toStringArray(predicate.getProduces());
return RequestMappingInfo.paths(paths.toArray(new String[0]))
.methods(method)
.consumes(consumes)
.produces(produces)
.build();
}
private void registerLinksMapping() {
String path = this.endpointMapping.getPath();
String linksPath = StringUtils.hasLength(path) ? path : "/";
String[] produces = StringUtils.toStringArray(this.endpointMediaTypes.getProduced());
RequestMappingInfo mapping = RequestMappingInfo.paths(linksPath)
.methods(RequestMethod.GET)
.produces(produces)
.build();
LinksHandler linksHandler = getLinksHandler();
registerMapping(mapping, linksHandler,
ReflectionUtils.findMethod(linksHandler.getClass(), "links", ServerWebExchange.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, ServerWebExchange exchange) {
CorsConfiguration corsConfiguration = super.getCorsConfiguration(handler, exchange);
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;
}
/**
* An {@link OperationInvoker} that performs the invocation of a blocking operation on
* a separate thread using Reactor's {@link Schedulers#boundedElastic() bounded
* elastic scheduler}.
*/
protected static final class ElasticSchedulerInvoker implements OperationInvoker {
private final OperationInvoker invoker;
public ElasticSchedulerInvoker(OperationInvoker invoker) {
this.invoker = invoker;
}
@Override
public Object invoke(InvocationContext context) {
return Mono.fromCallable(() -> this.invoker.invoke(context)).subscribeOn(Schedulers.boundedElastic());
}
}
protected static final class ExceptionCapturingInvoker implements OperationInvoker {
private final OperationInvoker invoker;
public ExceptionCapturingInvoker(OperationInvoker invoker) {
this.invoker = invoker;
}
@Override
public Object invoke(InvocationContext context) {
try {
return this.invoker.invoke(context);
}
catch (Exception ex) {
return Mono.error(ex);
}
}
}
/**
* Reactive handler providing actuator links at the root endpoint.
*/
@FunctionalInterface
protected interface LinksHandler {
Object links(ServerWebExchange exchange);
}
/**
* A reactive web operation that can be handled by WebFlux.
*/
@FunctionalInterface
protected interface ReactiveWebOperation {
Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body);
}
/**
* Adapter class to convert an {@link OperationInvoker} into a
* {@link ReactiveWebOperation}.
*/
private static final class ReactiveWebOperationAdapter implements ReactiveWebOperation {
private static final String PATH_SEPARATOR = AntPathMatcher.DEFAULT_PATH_SEPARATOR;
private final WebOperation operation;
private final OperationInvoker invoker;
private final Supplier<Mono<? extends SecurityContext>> securityContextSupplier;
private ReactiveWebOperationAdapter(WebOperation operation) {
this.operation = operation;
this.invoker = getInvoker(operation);
this.securityContextSupplier = getSecurityContextSupplier();
}
private OperationInvoker getInvoker(WebOperation operation) {
OperationInvoker invoker = operation::invoke;
if (operation.isBlocking()) {
return new ElasticSchedulerInvoker(invoker);
}
return new ExceptionCapturingInvoker(invoker);
}
private Supplier<Mono<? extends SecurityContext>> getSecurityContextSupplier() {
if (ClassUtils.isPresent("org.springframework.security.core.context.ReactiveSecurityContextHolder",
getClass().getClassLoader())) {
return this::springSecurityContext;
}
return this::emptySecurityContext;
}
Mono<? extends SecurityContext> springSecurityContext() {
return ReactiveSecurityContextHolder.getContext()
.map((securityContext) -> new ReactiveSecurityContext(securityContext.getAuthentication()))
.switchIfEmpty(Mono.just(new ReactiveSecurityContext(null)));
}
Mono<SecurityContext> emptySecurityContext() {
return Mono.just(SecurityContext.NONE);
}
@Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = getArguments(exchange, body);
OperationArgumentResolver serverNamespaceArgumentResolver = OperationArgumentResolver
.of(WebServerNamespace.class, () -> WebServerNamespace
.from(WebServerApplicationContext.getServerNamespace(exchange.getApplicationContext())));
return this.securityContextSupplier.get()
.map((securityContext) -> new InvocationContext(securityContext, arguments,
serverNamespaceArgumentResolver,
new ProducibleOperationArgumentResolver(
() -> exchange.getRequest().getHeaders().get("Accept"))))
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
exchange.getRequest().getMethod()));
}
private Map<String, Object> getArguments(ServerWebExchange exchange, Map<String, String> body) {
Map<String, Object> arguments = new LinkedHashMap<>(getTemplateVariables(exchange));
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable();
if (matchAllRemainingPathSegmentsVariable != null) {
arguments.put(matchAllRemainingPathSegmentsVariable, getRemainingPathSegments(exchange));
}
if (body != null) {
arguments.putAll(body);
}
exchange.getRequest()
.getQueryParams()
.forEach((name, values) -> arguments.put(name, (values.size() != 1) ? values : values.get(0)));
return arguments;
}
private Object getRemainingPathSegments(ServerWebExchange exchange) {
PathPattern pathPattern = exchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
if (pathPattern.hasPatternSyntax()) {
String remainingSegments = pathPattern
.extractPathWithinPattern(exchange.getRequest().getPath().pathWithinApplication())
.value();
return tokenizePathSegments(remainingSegments);
}
return tokenizePathSegments(pathPattern.toString());
}
private String[] tokenizePathSegments(String value) {
String[] segments = StringUtils.tokenizeToStringArray(value, PATH_SEPARATOR, false, true);
for (int i = 0; i < segments.length; i++) {
if (segments[i].contains("%")) {
segments[i] = StringUtils.uriDecode(segments[i], StandardCharsets.UTF_8);
}
}
return segments;
}
private Map<String, String> getTemplateVariables(ServerWebExchange exchange) {
return exchange.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
}
private Mono<ResponseEntity<Object>> handleResult(Publisher<?> result, HttpMethod httpMethod) {
if (result instanceof Flux) {
result = ((Flux<?>) result).collectList();
}
return Mono.from(result)
.map(this::toResponseEntity)
.onErrorMap(InvalidEndpointRequestException.class,
(ex) -> new ResponseStatusException(HttpStatus.BAD_REQUEST, ex.getReason()))
.defaultIfEmpty(new ResponseEntity<>(
(httpMethod != HttpMethod.GET) ? HttpStatus.NO_CONTENT : HttpStatus.NOT_FOUND));
}
private ResponseEntity<Object> toResponseEntity(Object response) {
if (!(response instanceof WebEndpointResponse<?> webEndpointResponse)) {
return new ResponseEntity<>(response, HttpStatus.OK);
}
MediaType contentType = (webEndpointResponse.getContentType() != null)
? new MediaType(webEndpointResponse.getContentType()) : null;
return ResponseEntity.status(webEndpointResponse.getStatus())
.contentType(contentType)
.body(webEndpointResponse.getBody());
}
@Override
public String toString() {
return "Actuator web endpoint '" + this.operation.getId() + "'";
}
}
/**
* Handler for a {@link ReactiveWebOperation}.
*/
private static final class WriteOperationHandler {
private final ReactiveWebOperation operation;
WriteOperationHandler(ReactiveWebOperation operation) {
this.operation = operation;
}
@ResponseBody
@Reflective
Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange,
@RequestBody(required = false) Map<String, String> body) {
return this.operation.handle(exchange, body);
}
@Override
public String toString() {
return this.operation.toString();
}
}
/**
* Handler for a {@link ReactiveWebOperation}.
*/
private static final class ReadOperationHandler {
private final ReactiveWebOperation operation;
ReadOperationHandler(ReactiveWebOperation operation) {
this.operation = operation;
}
@ResponseBody
@Reflective
Publisher<ResponseEntity<Object>> handle(ServerWebExchange exchange) {
return this.operation.handle(exchange, null);
}
@Override
public String toString() {
return this.operation.toString();
}
}
private static class WebFluxEndpointHandlerMethod extends HandlerMethod {
WebFluxEndpointHandlerMethod(Object bean, Method method) {
super(bean, method);
}
@Override
public String toString() {
return getBean().toString();
}
@Override
public HandlerMethod createWithResolvedBean() {
HandlerMethod handlerMethod = super.createWithResolvedBean();
return new WebFluxEndpointHandlerMethod(handlerMethod.getBean(), handlerMethod.getMethod());
}
}
private static final class ReactiveSecurityContext implements SecurityContext {
private static final String ROLE_PREFIX = "ROLE_";
private final Authentication authentication;
ReactiveSecurityContext(Authentication authentication) {
this.authentication = authentication;
}
private Authentication getAuthentication() {
return this.authentication;
}
@Override
public Principal getPrincipal() {
return this.authentication;
}
@Override
public boolean isUserInRole(String role) {
String authority = (!role.startsWith(ROLE_PREFIX)) ? ROLE_PREFIX + role : role;
AuthorizationResult result = AuthorityAuthorizationManager.hasAuthority(authority)
.authorize(this::getAuthentication, null);
return result != null && result.isGranted();
}
}
static class AbstractWebFluxEndpointHandlerMappingRuntimeHints implements RuntimeHintsRegistrar {
private final ReflectiveRuntimeHintsRegistrar reflectiveRegistrar = new ReflectiveRuntimeHintsRegistrar();
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
this.reflectiveRegistrar.registerRuntimeHints(hints, WriteOperationHandler.class,
ReadOperationHandler.class);
}
}
}

View File

@@ -0,0 +1,96 @@
/*
* 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.webflux.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.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
/**
* A custom {@link HandlerMapping} that allows health groups to be mapped to an additional
* path.
*
* @author Madhura Bhave
* @since 4.0.0
*/
public class AdditionalHealthEndpointPathsWebFluxHandlerMapping extends AbstractWebFluxEndpointHandlerMapping {
private final EndpointMapping endpointMapping;
private final ExposableWebEndpoint healthEndpoint;
private final Set<HealthEndpointGroup> groups;
public AdditionalHealthEndpointPathsWebFluxHandlerMapping(EndpointMapping endpointMapping,
ExposableWebEndpoint healthEndpoint, Set<HealthEndpointGroup> groups) {
super(endpointMapping, asList(healthEndpoint), null, null, false);
this.endpointMapping = endpointMapping;
this.groups = groups;
this.healthEndpoint = healthEndpoint;
}
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) {
RequestMappingInfo requestMappingInfo = getRequestMappingInfo(operation,
additionalPath.getValue());
registerReadMapping(requestMappingInfo, this.healthEndpoint, operation);
}
}
}
}
}
private RequestMappingInfo getRequestMappingInfo(WebOperation operation, String additionalPath) {
WebOperationRequestPredicate predicate = operation.getRequestPredicate();
String path = this.endpointMapping.createSubPath(additionalPath);
RequestMethod method = RequestMethod.valueOf(predicate.getHttpMethod().name());
String[] consumes = StringUtils.toStringArray(predicate.getConsumes());
String[] produces = StringUtils.toStringArray(predicate.getProduces());
return RequestMappingInfo.paths(path).methods(method).consumes(consumes).produces(produces).build();
}
@Override
protected LinksHandler getLinksHandler() {
return null;
}
}

View File

@@ -0,0 +1,164 @@
/*
* 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.webflux.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.reactive.HandlerMapping;
import org.springframework.web.reactive.result.method.RequestMappingInfo;
import org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern;
/**
* {@link HandlerMapping} that exposes
* {@link org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint @ControllerEndpoint}
* and
* {@link org.springframework.boot.actuate.endpoint.web.annotation.RestControllerEndpoint @RestControllerEndpoint}
* annotated endpoints over Spring WebFlux.
*
* @author Phillip Webb
* @since 4.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 = new HashSet<>(mapping.getMethodsCondition().getMethods());
if (methods.isEmpty()) {
methods.addAll(READ_ONLY_ACCESS_REQUEST_METHODS);
}
else {
methods.retainAll(READ_ONLY_ACCESS_REQUEST_METHODS);
}
return mapping.mutate().methods(methods.toArray(new RequestMethod[0])).build();
}
private RequestMappingInfo withEndpointMappedPatterns(ExposableControllerEndpoint endpoint,
RequestMappingInfo mapping) {
Set<PathPattern> patterns = mapping.getPatternsCondition().getPatterns();
if (patterns.isEmpty()) {
patterns = Collections.singleton(getPathPatternParser().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,117 @@
/*
* 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.webflux.actuate.endpoint.web;
import java.util.Collection;
import java.util.Collections;
import java.util.Map;
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.beans.factory.InitializingBean;
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.webflux.actuate.endpoint.web.WebFluxEndpointHandlerMapping.WebFluxEndpointHandlerMappingRuntimeHints;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.reactive.HandlerMapping;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
/**
* A custom {@link HandlerMapping} that makes web endpoints available over HTTP using
* Spring WebFlux.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @author Brian Clozel
* @since 4.0.0
*/
@ImportRuntimeHints(WebFluxEndpointHandlerMappingRuntimeHints.class)
public class WebFluxEndpointHandlerMapping extends AbstractWebFluxEndpointHandlerMapping implements InitializingBean {
private final EndpointLinksResolver linksResolver;
/**
* Creates a new {@code WebFluxEndpointHandlerMapping} 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 WebFluxEndpointHandlerMapping(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 WebFluxLinksHandler();
}
/**
* Handler for root endpoint providing links.
*/
class WebFluxLinksHandler implements LinksHandler {
@Override
@ResponseBody
@Reflective
public Map<String, Map<String, Link>> links(ServerWebExchange exchange) {
String requestUri = UriComponentsBuilder.fromUri(exchange.getRequest().getURI())
.replaceQuery(null)
.toUriString();
Map<String, Link> links = WebFluxEndpointHandlerMapping.this.linksResolver.resolveLinks(requestUri);
return OperationResponseBody.of(Collections.singletonMap("_links", links));
}
@Override
public String toString() {
return "Actuator root web endpoint";
}
}
static class WebFluxEndpointHandlerMappingRuntimeHints 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, WebFluxLinksHandler.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 WebFlux support for actuator endpoints.
*/
package org.springframework.boot.webflux.actuate.endpoint.web;

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.webflux.actuate.web.exchanges;
import java.security.Principal;
import java.util.Set;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.web.exchanges.HttpExchange;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.Include;
import org.springframework.core.Ordered;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import org.springframework.web.server.WebSession;
/**
* A {@link WebFilter} for recording {@link HttpExchange HTTP exchanges}.
*
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.0.0
*/
public class HttpExchangesWebFilter implements WebFilter, Ordered {
private static final Object NONE = new Object();
// Not LOWEST_PRECEDENCE, but near the end, so it has a good chance of catching all
// enriched headers, but users can add stuff after this if they want to
private int order = Ordered.LOWEST_PRECEDENCE - 10;
private final HttpExchangeRepository repository;
private final Set<Include> includes;
/**
* Create a new {@link HttpExchangesWebFilter} instance.
* @param repository the repository used to record events
* @param includes the include options
*/
public HttpExchangesWebFilter(HttpExchangeRepository repository, Set<Include> includes) {
this.repository = repository;
this.includes = includes;
}
@Override
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Mono<?> principal = exchange.getPrincipal().cast(Object.class).defaultIfEmpty(NONE);
Mono<Object> session = exchange.getSession().cast(Object.class).defaultIfEmpty(NONE);
return Mono.zip(PrincipalAndSession::new, principal, session)
.flatMap((principalAndSession) -> filter(exchange, chain, principalAndSession));
}
private Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain,
PrincipalAndSession principalAndSession) {
return Mono.fromRunnable(() -> addExchangeOnCommit(exchange, principalAndSession)).and(chain.filter(exchange));
}
private void addExchangeOnCommit(ServerWebExchange exchange, PrincipalAndSession principalAndSession) {
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(exchange.getRequest());
HttpExchange.Started startedHttpExchange = HttpExchange.start(sourceRequest);
exchange.getResponse().beforeCommit(() -> {
RecordableServerHttpResponse sourceResponse = new RecordableServerHttpResponse(exchange.getResponse());
HttpExchange finishedExchange = startedHttpExchange.finish(sourceResponse,
principalAndSession::getPrincipal, principalAndSession::getSessionId, this.includes);
this.repository.add(finishedExchange);
return Mono.empty();
});
}
/**
* A {@link Principal} and {@link WebSession}.
*/
private static class PrincipalAndSession {
private final Principal principal;
private final WebSession session;
PrincipalAndSession(Object[] zipped) {
this.principal = (zipped[0] != NONE) ? (Principal) zipped[0] : null;
this.session = (zipped[1] != NONE) ? (WebSession) zipped[1] : null;
}
Principal getPrincipal() {
return this.principal;
}
String getSessionId() {
return (this.session != null && this.session.isStarted()) ? this.session.getId() : null;
}
}
}

View File

@@ -0,0 +1,81 @@
/*
* 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.webflux.actuate.web.exchanges;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.web.exchanges.RecordableHttpRequest;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
/**
* A {@link RecordableHttpRequest} backed by a {@link ServerHttpRequest}.
*
* @author Andy Wilkinson
*/
class RecordableServerHttpRequest implements RecordableHttpRequest {
private final String method;
private final HttpHeaders headers;
private final URI uri;
private final String remoteAddress;
RecordableServerHttpRequest(ServerHttpRequest request) {
this.method = request.getMethod().name();
this.headers = request.getHeaders();
this.uri = request.getURI();
this.remoteAddress = getRemoteAddress(request);
}
private static String getRemoteAddress(ServerHttpRequest request) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
InetAddress address = (remoteAddress != null) ? remoteAddress.getAddress() : null;
return (address != null) ? address.toString() : null;
}
@Override
public String getMethod() {
return this.method;
}
@Override
public URI getUri() {
return this.uri;
}
@Override
public Map<String, List<String>> getHeaders() {
Map<String, List<String>> headers = new LinkedHashMap<>();
this.headers.forEach(headers::put);
return Collections.unmodifiableMap(headers);
}
@Override
public String getRemoteAddress() {
return this.remoteAddress;
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.webflux.actuate.web.exchanges;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.boot.actuate.web.exchanges.RecordableHttpResponse;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
/**
* An adapter that exposes a {@link ServerHttpResponse} as a
* {@link RecordableHttpResponse}.
*
* @author Andy Wilkinson
*/
class RecordableServerHttpResponse implements RecordableHttpResponse {
private final int status;
private final Map<String, List<String>> headers;
RecordableServerHttpResponse(ServerHttpResponse response) {
this.status = (response.getStatusCode() != null) ? response.getStatusCode().value() : HttpStatus.OK.value();
Map<String, List<String>> headers = new LinkedHashMap<>();
response.getHeaders().forEach(headers::put);
this.headers = Collections.unmodifiableMap(headers);
}
@Override
public int getStatus() {
return this.status;
}
@Override
public Map<String, List<String>> getHeaders() {
return this.headers;
}
}

View File

@@ -0,0 +1,22 @@
/*
* 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 HTTP exchanges support for reactive servers.
*
* @see org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository
*/
package org.springframework.boot.webflux.actuate.web.exchanges;

View File

@@ -0,0 +1,2 @@
org.springframework.boot.webflux.actuate.autoconfigure.endpoint.web.WebFluxEndpointManagementContextConfiguration
org.springframework.boot.webflux.actuate.autoconfigure.web.WebFluxManagementChildContextConfiguration

View File

@@ -1,3 +1,4 @@
org.springframework.boot.webflux.actuate.autoconfigure.health.WebFluxHealthEndpointExtensionAutoConfiguration
org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration
org.springframework.boot.webflux.autoconfigure.ReactiveMultipartAutoConfiguration
org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration

View File

@@ -0,0 +1,85 @@
/*
* 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.webflux.actuate.autoconfigure.health;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.condition.WithTestEndpointOutcomeExposureContributor;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthContributorAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.health.HealthEndpointAutoConfiguration;
import org.springframework.boot.actuate.endpoint.web.WebEndpointsSupplier;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthEndpoint;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.boot.actuate.health.ReactiveHealthEndpointWebExtension;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.webflux.actuate.endpoint.web.AdditionalHealthEndpointPathsWebFluxHandlerMapping;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebFluxHealthEndpointExtensionAutoConfiguration}.
*
* @author Stephane Nicoll
*/
class WebFluxHealthEndpointExtensionAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(HealthIndicatorsConfiguration.class)
.withConfiguration(AutoConfigurations.of(HealthContributorAutoConfiguration.class,
HealthEndpointAutoConfiguration.class, WebFluxHealthEndpointExtensionAutoConfiguration.class));
@Test
@WithTestEndpointOutcomeExposureContributor
void additionalReactiveHealthEndpointsPathsTolerateHealthEndpointThatIsNotWebExposed() {
this.contextRunner
.withConfiguration(
AutoConfigurations.of(EndpointAutoConfiguration.class, WebEndpointAutoConfiguration.class))
.withPropertyValues("management.endpoints.web.exposure.exclude=*",
"management.endpoints.test.exposure.include=*")
.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(HealthEndpoint.class);
assertThat(context).hasSingleBean(ReactiveHealthEndpointWebExtension.class);
assertThat(context.getBean(WebEndpointsSupplier.class).getEndpoints()).isEmpty();
assertThat(context).hasSingleBean(AdditionalHealthEndpointPathsWebFluxHandlerMapping.class);
});
}
@Configuration(proxyBeanMethods = false)
static class HealthIndicatorsConfiguration {
@Bean
ReactiveHealthIndicator simpleHealthIndicator() {
return () -> Mono.just(Health.up().build());
}
@Bean
HealthIndicator additionalHealthIndicator() {
return () -> Health.up().build();
}
}
}

View File

@@ -0,0 +1,170 @@
/*
* 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.webflux.actuate.autoconfigure.web;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import org.apache.catalina.Valve;
import org.apache.catalina.startup.Tomcat;
import org.apache.catalina.valves.AccessLogValve;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.springframework.boot.actuate.autoconfigure.endpoint.EndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointAutoConfiguration;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementContextAutoConfiguration;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.boot.env.ConfigTreePropertySource;
import org.springframework.boot.test.context.assertj.AssertableReactiveWebApplicationContext;
import org.springframework.boot.test.context.runner.ContextConsumer;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.boot.tomcat.TomcatWebServer;
import org.springframework.boot.tomcat.actuate.autoconfigure.web.TomcatReactiveManagementContextAutoConfiguration;
import org.springframework.boot.tomcat.autoconfigure.reactive.TomcatReactiveWebServerAutoConfiguration;
import org.springframework.boot.web.server.WebServer;
import org.springframework.boot.web.server.context.ServerPortInfoApplicationContextInitializer;
import org.springframework.boot.web.server.context.WebServerInitializedEvent;
import org.springframework.boot.web.server.reactive.context.AnnotationConfigReactiveWebServerApplicationContext;
import org.springframework.boot.webflux.autoconfigure.HttpHandlerAutoConfiguration;
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.http.MediaType;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.reactive.function.client.WebClient;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Integration tests for {@link WebFluxManagementChildContextConfiguration}.
*
* @author Andy Wilkinson
*/
class WebFluxManagementChildContextConfigurationIntegrationTests {
private final List<WebServer> webServers = new ArrayList<>();
private final ReactiveWebApplicationContextRunner runner = new ReactiveWebApplicationContextRunner(
AnnotationConfigReactiveWebServerApplicationContext::new)
.withConfiguration(AutoConfigurations.of(ManagementContextAutoConfiguration.class,
TomcatReactiveWebServerAutoConfiguration.class, TomcatReactiveManagementContextAutoConfiguration.class,
WebEndpointAutoConfiguration.class, EndpointAutoConfiguration.class, HttpHandlerAutoConfiguration.class,
WebFluxAutoConfiguration.class))
.withUserConfiguration(SucceedingEndpoint.class)
.withInitializer(new ServerPortInfoApplicationContextInitializer())
.withInitializer((context) -> context.addApplicationListener(
(ApplicationListener<WebServerInitializedEvent>) (event) -> this.webServers.add(event.getWebServer())))
.withPropertyValues("server.port=0", "management.server.port=0", "management.endpoints.web.exposure.include=*");
@TempDir
Path temp;
@Test
void endpointsAreBeneathActuatorByDefault() {
this.runner.withPropertyValues("management.server.port:0").run(withWebTestClient((client) -> {
String body = client.get()
.uri("actuator/success")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class))
.block();
assertThat(body).isEqualTo("Success");
}));
}
@Test
void whenManagementServerBasePathIsConfiguredThenEndpointsAreBeneathThatPath() {
this.runner.withPropertyValues("management.server.port:0", "management.server.base-path:/manage")
.run(withWebTestClient((client) -> {
String body = client.get()
.uri("manage/actuator/success")
.accept(MediaType.APPLICATION_JSON)
.exchangeToMono((response) -> response.bodyToMono(String.class))
.block();
assertThat(body).isEqualTo("Success");
}));
}
@Test // gh-32941
void whenManagementServerPortLoadedFromConfigTree() {
this.runner.withInitializer(this::addConfigTreePropertySource)
.run((context) -> assertThat(context).hasNotFailed());
}
@Test
void accessLogHasManagementServerSpecificPrefix() {
this.runner.withPropertyValues("server.tomcat.accesslog.enabled=true").run((context) -> {
AccessLogValve accessLogValve = findAccessLogValve();
assertThat(accessLogValve).isNotNull();
assertThat(accessLogValve.getPrefix()).isEqualTo("management_access_log");
});
}
private AccessLogValve findAccessLogValve() {
assertThat(this.webServers).hasSize(2);
Tomcat tomcat = ((TomcatWebServer) this.webServers.get(1)).getTomcat();
for (Valve valve : tomcat.getEngine().getPipeline().getValves()) {
if (valve instanceof AccessLogValve accessLogValve) {
return accessLogValve;
}
}
return null;
}
private void addConfigTreePropertySource(ConfigurableApplicationContext applicationContext) {
try {
applicationContext.getEnvironment()
.setConversionService((ConfigurableConversionService) ApplicationConversionService.getSharedInstance());
Path configtree = this.temp.resolve("configtree");
Path file = configtree.resolve("management/server/port");
file.toFile().getParentFile().mkdirs();
FileCopyUtils.copy("0".getBytes(StandardCharsets.UTF_8), file.toFile());
ConfigTreePropertySource source = new ConfigTreePropertySource("configtree", configtree);
applicationContext.getEnvironment().getPropertySources().addFirst(source);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
private ContextConsumer<AssertableReactiveWebApplicationContext> withWebTestClient(Consumer<WebClient> webClient) {
return (context) -> {
String port = context.getEnvironment().getProperty("local.management.port");
WebClient client = WebClient.create("http://localhost:" + port);
webClient.accept(client);
};
}
@Endpoint(id = "success")
static class SucceedingEndpoint {
@ReadOperation
String fail() {
return "Success";
}
}
}

View File

@@ -0,0 +1,51 @@
/*
* 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.webflux.actuate.autoconfigure.web;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.autoconfigure.web.server.ManagementServerProperties;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link WebFluxManagementChildContextConfiguration}.
*
* @author Moritz Halbritter
*/
class WebFluxManagementChildContextConfigurationTests {
@Test
// gh-45857
void failsWithoutManagementServerPropertiesBeanFromParent() {
new ReactiveWebApplicationContextRunner()
.run((parent) -> new ReactiveWebApplicationContextRunner().withParent(parent)
.withUserConfiguration(WebFluxManagementChildContextConfiguration.class)
.run((context) -> assertThat(context).hasFailed()));
}
@Test
// gh-45857
void succeedsWithManagementServerPropertiesBeanFromParent() {
new ReactiveWebApplicationContextRunner().withBean(ManagementServerProperties.class)
.run((parent) -> new ReactiveWebApplicationContextRunner().withParent(parent)
.withUserConfiguration(WebFluxManagementChildContextConfiguration.class)
.run((context) -> assertThat(context).hasNotFailed()));
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.webflux.actuate.endpoint.web;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.aot.hint.predicate.RuntimeHintsPredicates;
import org.springframework.boot.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping.AbstractWebFluxEndpointHandlerMappingRuntimeHints;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link AbstractWebFluxEndpointHandlerMapping}.
*
* @author Moritz Halbritter
*/
class AbstractWebFluxEndpointHandlerMappingTests {
@Test
void shouldRegisterHints() {
RuntimeHints runtimeHints = new RuntimeHints();
new AbstractWebFluxEndpointHandlerMappingRuntimeHints().registerHints(runtimeHints,
getClass().getClassLoader());
assertThat(RuntimeHintsPredicates.reflection()
.onType(TypeReference
.of("org.springframework.boot.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping.WriteOperationHandler")))
.accepts(runtimeHints);
assertThat(RuntimeHintsPredicates.reflection()
.onType(TypeReference
.of("org.springframework.boot.webflux.actuate.endpoint.web.AbstractWebFluxEndpointHandlerMapping.ReadOperationHandler")))
.accepts(runtimeHints);
}
}

View File

@@ -0,0 +1,169 @@
/*
* 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.webflux.actuate.endpoint.web;
import java.time.Duration;
import java.util.Arrays;
import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.endpoint.Access;
import org.springframework.boot.actuate.endpoint.EndpointId;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.annotation.ControllerEndpoint;
import org.springframework.boot.actuate.endpoint.web.annotation.ExposableControllerEndpoint;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.server.MethodNotAllowedException;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link ControllerEndpointHandlerMapping}.
*
* @author Phillip Webb
* @author Stephane Nicoll
* @deprecated since 3.3.5 in favor of {@code @Endpoint} and {@code @WebEndpoint} support
*/
@Deprecated(since = "3.3.5", forRemoval = true)
@SuppressWarnings("removal")
class ControllerEndpointHandlerMappingTests {
private final StaticApplicationContext context = new StaticApplicationContext();
@Test
void mappingWithNoPrefix() {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("", first, second);
assertThat(getHandler(mapping, HttpMethod.GET, "/first")).isEqualTo(handlerOf(first.getController(), "get"));
assertThat(getHandler(mapping, HttpMethod.POST, "/second"))
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(getHandler(mapping, HttpMethod.GET, "/third")).isNull();
}
@Test
void mappingWithPrefix() {
ExposableControllerEndpoint first = firstEndpoint();
ExposableControllerEndpoint second = secondEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first, second);
assertThat(getHandler(mapping, HttpMethod.GET, "/actuator/first"))
.isEqualTo(handlerOf(first.getController(), "get"));
assertThat(getHandler(mapping, HttpMethod.POST, "/actuator/second"))
.isEqualTo(handlerOf(second.getController(), "save"));
assertThat(getHandler(mapping, HttpMethod.GET, "/first")).isNull();
assertThat(getHandler(mapping, HttpMethod.GET, "/second")).isNull();
}
@Test
void mappingWithNoPath() {
ExposableControllerEndpoint pathless = pathlessEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", pathless);
assertThat(getHandler(mapping, HttpMethod.GET, "/actuator/pathless"))
.isEqualTo(handlerOf(pathless.getController(), "get"));
assertThat(getHandler(mapping, HttpMethod.GET, "/pathless")).isNull();
assertThat(getHandler(mapping, HttpMethod.GET, "/")).isNull();
}
@Test
void mappingNarrowedToMethod() {
ExposableControllerEndpoint first = firstEndpoint();
ControllerEndpointHandlerMapping mapping = createMapping("actuator", first);
assertThatExceptionOfType(MethodNotAllowedException.class)
.isThrownBy(() -> getHandler(mapping, HttpMethod.POST, "/actuator/first"));
}
private Object getHandler(ControllerEndpointHandlerMapping mapping, HttpMethod method, String requestURI) {
return mapping.getHandler(exchange(method, requestURI)).block(Duration.ofSeconds(30));
}
private ControllerEndpointHandlerMapping createMapping(String prefix, ExposableControllerEndpoint... endpoints) {
ControllerEndpointHandlerMapping mapping = new ControllerEndpointHandlerMapping(new EndpointMapping(prefix),
Arrays.asList(endpoints), null, (endpointId, defaultAccess) -> Access.UNRESTRICTED);
mapping.setApplicationContext(this.context);
mapping.afterPropertiesSet();
return mapping;
}
private HandlerMethod handlerOf(Object source, String methodName) {
return new HandlerMethod(source, ReflectionUtils.findMethod(source.getClass(), methodName));
}
private MockServerWebExchange exchange(HttpMethod method, String requestURI) {
return MockServerWebExchange.from(MockServerHttpRequest.method(method, requestURI).build());
}
private ExposableControllerEndpoint firstEndpoint() {
return mockEndpoint(EndpointId.of("first"), new FirstTestMvcEndpoint());
}
private ExposableControllerEndpoint secondEndpoint() {
return mockEndpoint(EndpointId.of("second"), new SecondTestMvcEndpoint());
}
private ExposableControllerEndpoint pathlessEndpoint() {
return mockEndpoint(EndpointId.of("pathless"), new PathlessControllerEndpoint());
}
private ExposableControllerEndpoint mockEndpoint(EndpointId id, Object controller) {
ExposableControllerEndpoint endpoint = mock(ExposableControllerEndpoint.class);
given(endpoint.getEndpointId()).willReturn(id);
given(endpoint.getController()).willReturn(controller);
given(endpoint.getRootPath()).willReturn(id.toString());
return endpoint;
}
@ControllerEndpoint(id = "first")
static class FirstTestMvcEndpoint {
@GetMapping("/")
String get() {
return "test";
}
}
@ControllerEndpoint(id = "second")
static class SecondTestMvcEndpoint {
@PostMapping("/")
void save() {
}
}
@ControllerEndpoint(id = "pathless")
static class PathlessControllerEndpoint {
@GetMapping
String get() {
return "test";
}
}
}

View File

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

View File

@@ -0,0 +1,131 @@
/*
* 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.webflux.actuate.web.exchanges;
import java.util.EnumSet;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.boot.actuate.web.exchanges.HttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.Include;
import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.HandlerFunction;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.adapter.WebHttpHandlerBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* Integration tests for {@link HttpExchangesWebFilter}.
*
* @author Andy Wilkinson
*/
class HttpExchangesWebFilterIntegrationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withUserConfiguration(Config.class);
@Test
void exchangeForNotFoundResponseHas404Status() {
this.contextRunner.run((context) -> {
WebTestClient.bindToApplicationContext(context)
.build()
.get()
.uri("/")
.exchange()
.expectStatus()
.isNotFound();
HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(404);
});
}
@Test
void exchangeForMonoErrorWithRuntimeExceptionHas500Status() {
this.contextRunner.run((context) -> {
WebTestClient.bindToApplicationContext(context)
.build()
.get()
.uri("/mono-error")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(500);
});
}
@Test
void exchangeForThrownRuntimeExceptionHas500Status() {
this.contextRunner.run((context) -> {
WebTestClient.bindToApplicationContext(context)
.build()
.get()
.uri("/thrown")
.exchange()
.expectStatus()
.isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
HttpExchangeRepository repository = context.getBean(HttpExchangeRepository.class);
assertThat(repository.findAll()).hasSize(1);
assertThat(repository.findAll().get(0).getResponse().getStatus()).isEqualTo(500);
});
}
@Configuration(proxyBeanMethods = false)
@EnableWebFlux
static class Config {
@Bean
HttpExchangesWebFilter httpExchangesWebFilter(HttpExchangeRepository repository) {
return new HttpExchangesWebFilter(repository, EnumSet.allOf(Include.class));
}
@Bean
HttpExchangeRepository httpExchangeRepository() {
return new InMemoryHttpExchangeRepository();
}
@Bean
HttpHandler httpHandler(ApplicationContext applicationContext) {
return WebHttpHandlerBuilder.applicationContext(applicationContext).build();
}
@Bean
RouterFunction<ServerResponse> router() {
return route(GET("/mono-error"), (request) -> Mono.error(new RuntimeException())).andRoute(GET("/thrown"),
(HandlerFunction<ServerResponse>) (request) -> {
throw new RuntimeException();
});
}
}
}

View File

@@ -0,0 +1,110 @@
/*
* 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.webflux.actuate.web.exchanges;
import java.security.Principal;
import java.time.Duration;
import java.util.EnumSet;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.boot.actuate.web.exchanges.HttpExchange.Session;
import org.springframework.boot.actuate.web.exchanges.InMemoryHttpExchangeRepository;
import org.springframework.boot.actuate.web.exchanges.Include;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
import org.springframework.mock.web.server.MockServerWebExchange;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.ServerWebExchangeDecorator;
import org.springframework.web.server.WebFilterChain;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link HttpExchangesWebFilter}.
*
* @author Andy Wilkinson
*/
class HttpExchangesWebFilterTests {
private final InMemoryHttpExchangeRepository repository = new InMemoryHttpExchangeRepository();
private final HttpExchangesWebFilter filter = new HttpExchangesWebFilter(this.repository,
EnumSet.allOf(Include.class));
@Test
void filterRecordsExchange() {
executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")),
(exchange) -> Mono.empty());
assertThat(this.repository.findAll()).hasSize(1);
}
@Test
void filterRecordsSessionIdWhenSessionIsUsed() {
executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")),
(exchange) -> exchange.getSession()
.doOnNext((session) -> session.getAttributes().put("a", "alpha"))
.then());
assertThat(this.repository.findAll()).hasSize(1);
Session session = this.repository.findAll().get(0).getSession();
assertThat(session).isNotNull();
assertThat(session.getId()).isNotNull();
}
@Test
void filterDoesNotRecordIdOfUnusedSession() {
executeFilter(MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com")),
(exchange) -> exchange.getSession().then());
assertThat(this.repository.findAll()).hasSize(1);
Session session = this.repository.findAll().get(0).getSession();
assertThat(session).isNull();
}
@Test
void filterRecordsPrincipal() {
Principal principal = mock(Principal.class);
given(principal.getName()).willReturn("alice");
executeFilter(new ServerWebExchangeDecorator(
MockServerWebExchange.from(MockServerHttpRequest.get("https://api.example.com"))) {
@SuppressWarnings("unchecked")
@Override
public <T extends Principal> Mono<T> getPrincipal() {
return Mono.just((T) principal);
}
}, (exchange) -> exchange.getSession().doOnNext((session) -> session.getAttributes().put("a", "alpha")).then());
assertThat(this.repository.findAll()).hasSize(1);
org.springframework.boot.actuate.web.exchanges.HttpExchange.Principal recordedPrincipal = this.repository
.findAll()
.get(0)
.getPrincipal();
assertThat(recordedPrincipal).isNotNull();
assertThat(recordedPrincipal.getName()).isEqualTo("alice");
}
private void executeFilter(ServerWebExchange exchange, WebFilterChain chain) {
StepVerifier
.create(this.filter.filter(exchange, chain).then(Mono.defer(() -> exchange.getResponse().setComplete())))
.expectComplete()
.verify(Duration.ofSeconds(30));
}
}

View File

@@ -0,0 +1,94 @@
/*
* 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.webflux.actuate.web.exchanges;
import java.net.InetSocketAddress;
import java.net.URI;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link RecordableServerHttpRequest}.
*
* @author Dmytro Nosan
*/
class RecordableServerHttpRequestTests {
private ServerWebExchange exchange;
private ServerHttpRequest request;
@BeforeEach
void setUp() {
this.exchange = mock(ServerWebExchange.class);
this.request = mock(ServerHttpRequest.class);
given(this.exchange.getRequest()).willReturn(this.request);
given(this.request.getMethod()).willReturn(HttpMethod.GET);
}
@Test
void getMethod() {
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(this.request);
assertThat(sourceRequest.getMethod()).isEqualTo("GET");
}
@Test
void getUri() {
URI uri = URI.create("http://localhost:8080/");
given(this.request.getURI()).willReturn(uri);
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(this.request);
assertThat(sourceRequest.getUri()).isSameAs(uri);
}
@Test
void getHeaders() {
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.add("name", "value");
given(this.request.getHeaders()).willReturn(httpHeaders);
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(this.request);
assertThat(sourceRequest.getHeaders()).containsOnly(entry("name", Collections.singletonList("value")));
}
@Test
void getUnresolvedRemoteAddress() {
InetSocketAddress socketAddress = InetSocketAddress.createUnresolved("unresolved.example.com", 8080);
given(this.request.getRemoteAddress()).willReturn(socketAddress);
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(this.request);
assertThat(sourceRequest.getRemoteAddress()).isNull();
}
@Test
void getRemoteAddress() {
InetSocketAddress socketAddress = new InetSocketAddress(0);
given(this.request.getRemoteAddress()).willReturn(socketAddress);
RecordableServerHttpRequest sourceRequest = new RecordableServerHttpRequest(this.request);
assertThat(sourceRequest.getRemoteAddress()).isEqualTo(socketAddress.getAddress().toString());
}
}

View File

@@ -23,10 +23,10 @@ import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
import org.springframework.boot.actuate.endpoint.web.annotation.WebEndpointDiscoverer;
import org.springframework.boot.actuate.endpoint.web.reactive.WebFluxEndpointHandlerMapping;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.jackson.autoconfigure.JacksonAutoConfiguration;
import org.springframework.boot.reactor.netty.NettyReactiveWebServerFactory;
import org.springframework.boot.webflux.actuate.endpoint.web.WebFluxEndpointHandlerMapping;
import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;