Move Actuator infrastructure for WebFlux to spring-boot-webflux
This commit is contained in:
committed by
Phillip Webb
parent
b9aa01c1c4
commit
e0a6237b8a
@@ -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")
|
||||
|
||||
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,2 @@
|
||||
org.springframework.boot.webflux.actuate.autoconfigure.endpoint.web.WebFluxEndpointManagementContextConfiguration
|
||||
org.springframework.boot.webflux.actuate.autoconfigure.web.WebFluxManagementChildContextConfiguration
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user