Merge origin/4.1.x

Signed-off-by: Olga Maciaszek-Sharma <olga.maciaszek-sharma@broadcom.com>
This commit is contained in:
Olga Maciaszek-Sharma
2025-05-27 20:58:35 +02:00
6 changed files with 174 additions and 131 deletions

View File

@@ -31,9 +31,9 @@ import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings.Redirects;
import org.springframework.boot.web.client.RestClientCustomizer;
import org.springframework.cloud.gateway.server.mvc.common.ArgumentSupplierBeanPostProcessor;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcAotRuntimeHintsRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcPropertiesBeanDefinitionRegistrar;
import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcRuntimeHintsProcessor;
import org.springframework.cloud.gateway.server.mvc.config.RouterFunctionHolderFactory;
import org.springframework.cloud.gateway.server.mvc.filter.FormFilter;
import org.springframework.cloud.gateway.server.mvc.filter.ForwardedRequestHeadersFilter;
@@ -54,7 +54,6 @@ import org.springframework.cloud.gateway.server.mvc.predicate.PredicateDiscovere
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MapPropertySource;
@@ -73,7 +72,6 @@ import org.springframework.web.client.RestClient;
RestClientAutoConfiguration.class })
@ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true)
@Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class)
@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class)
public class GatewayServerMvcAutoConfiguration {
@Bean
@@ -199,6 +197,11 @@ public class GatewayServerMvcAutoConfiguration {
return new XForwardedRequestHeadersFilterProperties();
}
@Bean
static GatewayMvcRuntimeHintsProcessor gatewayMvcRuntimeHintsProcessor() {
return new GatewayMvcRuntimeHintsProcessor();
}
static class GatewayHttpClientEnvironmentPostProcessor implements EnvironmentPostProcessor {
static final boolean APACHE = ClassUtils.isPresent("org.apache.hc.client5.http.impl.classic.HttpClients", null);

View File

@@ -1,82 +0,0 @@
/*
* Copyright 2013-2024 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.cloud.gateway.server.mvc.config;
import java.util.Arrays;
import java.util.Set;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.cloud.gateway.server.mvc.filter.AfterFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.BeforeFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.BodyFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.FilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerHandlerSupplier;
import org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.GatewayRouterFunctions;
import org.springframework.cloud.gateway.server.mvc.handler.HandlerFunctions;
import org.springframework.cloud.gateway.server.mvc.predicate.GatewayRequestPredicates;
import org.springframework.lang.NonNull;
import org.springframework.util.ClassUtils;
/**
* AOT runtime hints registrar on the gateway server mvc.
*
* @author Jürgen Wißkirchen
*/
public class GatewayMvcAotRuntimeHintsRegistrar implements RuntimeHintsRegistrar {
private static final Set<Class<?>> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class,
LoadBalancerHandlerSupplier.class, FilterFunctions.class, BeforeFilterFunctions.class,
AfterFilterFunctions.class, TokenRelayFilterFunctions.class, BodyFilterFunctions.class,
CircuitBreakerFilterFunctions.class, GatewayRouterFunctions.class, LoadBalancerFilterFunctions.class,
GatewayRequestPredicates.class, Bucket4jFilterFunctions.class);
private static final Set<Class<?>> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class,
RouteProperties.class);
@Override
public void registerHints(@NonNull RuntimeHints hints, ClassLoader classLoader) {
final ReflectionHints reflectionHints = hints.reflection();
FUNCTION_PROVIDERS.forEach(clazz -> addHintsForClass(reflectionHints, clazz, classLoader));
PROPERTIES.forEach(clazz -> reflectionHints.registerType(clazz, MemberCategory.PUBLIC_FIELDS,
MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS));
}
/**
* Add hints for the given class. Since we need to register mostly static methods, the
* annotation way with @Reflective does not work here.
* @param reflectionHints the reflection hints
* @param clazz the class to add hints for
* @param classLoader the class loader
*/
private void addHintsForClass(ReflectionHints reflectionHints, Class<?> clazz, ClassLoader classLoader) {
if (!ClassUtils.isPresent(clazz.getName(), classLoader)) {
return; // safety net
}
Arrays.stream(clazz.getMethods())
.forEach(method -> reflectionHints.registerMethod(method, ExecutableMode.INVOKE));
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2013-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.cloud.gateway.server.mvc.config;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution;
import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.core.type.filter.AssignableTypeFilter;
/**
* A {@link BeanFactoryInitializationAotProcessor} responsible for registering reflection
* hints for Gateway MVC beans.
*
* @author Jürgen Wißkirchen
* @author Olga Maciaszek-Sharma
* @since 4.3.0
*/
public class GatewayMvcRuntimeHintsProcessor implements BeanFactoryInitializationAotProcessor {
private static final Log LOG = LogFactory.getLog(GatewayMvcRuntimeHintsProcessor.class);
private static final String GATEWAY_MVC_FILTER_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.filter";
private static final String GATEWAY_MVC_PREDICATE_PACKAGE_NAME = "org.springframework.cloud.gateway.server.mvc.predicate";
private static final Map<String, Set<String>> beansConditionalOnClasses = Map.of(
"io.github.bucket4j.BucketConfiguration",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.Bucket4jFilterFunctions"),
"org.springframework.cloud.client.circuitbreaker.CircuitBreaker",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.CircuitBreakerFilterFunctions"),
"org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.LoadBalancerFilterFunctions"),
"org.springframework.retry.support.RetryTemplate",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.RetryFilterFunctions"),
"org.springframework.security.oauth2.client.OAuth2AuthorizedClient",
Set.of("org.springframework.cloud.gateway.server.mvc.filter.TokenRelayFilterFunctions"));
private static final Set<Class<?>> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class,
RouteProperties.class);
@Override
public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) {
return (generationContext, beanFactoryInitializationCode) -> {
ReflectionHints hints = generationContext.getRuntimeHints().reflection();
Set<Class<?>> typesToRegister = Stream
.of(getTypesToRegister(GATEWAY_MVC_FILTER_PACKAGE_NAME),
getTypesToRegister(GATEWAY_MVC_PREDICATE_PACKAGE_NAME), PROPERTIES)
.flatMap(Set::stream)
.collect(Collectors.toSet());
typesToRegister.forEach(clazz -> hints.registerType(TypeReference.of(clazz),
hint -> hint.withMembers(MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_METHODS,
MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)));
};
}
private static Set<Class<?>> getTypesToRegister(String packageName) {
Set<Class<?>> classesToAdd = new HashSet<>();
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new AssignableTypeFilter(Object.class));
Set<BeanDefinition> components = provider.findCandidateComponents(packageName);
for (BeanDefinition component : components) {
Class<?> clazz;
try {
clazz = Class.forName(component.getBeanClassName());
if (shouldRegisterClass(clazz)) {
classesToAdd.add(clazz);
}
}
catch (NoClassDefFoundError | ClassNotFoundException exception) {
if (LOG.isDebugEnabled()) {
LOG.debug(exception);
}
}
}
return classesToAdd;
}
private static boolean shouldRegisterClass(Class<?> clazz) {
Set<String> conditionClasses = beansConditionalOnClasses.getOrDefault(clazz.getName(), Collections.emptySet());
for (String conditionClass : conditionClasses) {
try {
GatewayMvcRuntimeHintsProcessor.class.getClassLoader().loadClass(conditionClass);
}
catch (ClassNotFoundException e) {
return false;
}
}
return true;
}
}

View File

@@ -60,58 +60,58 @@ class AfterFilterFunctionsTests {
@Test
void doesNotRemoveJsonAttributes() {
restClient.get()
.uri("/anything/does_not/remove_json_attributes")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsEntry("foo", "bar");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
.uri("/anything/does_not/remove_json_attributes")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsEntry("foo", "bar");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
}
@Test
void removeJsonAttributesToAvoidBeingRecursive() {
restClient.get()
.uri("/anything/remove_json_attributes_to_avoid_being_recursive")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).doesNotContainKey("foo");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
.uri("/anything/remove_json_attributes_to_avoid_being_recursive")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).doesNotContainKey("foo");
assertThat(res.getResponseBody()).containsEntry("baz", "qux");
});
}
@Test
void removeJsonAttributesRecursively() {
restClient.get()
.uri("/anything/remove_json_attributes_recursively")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsKey("foo");
assertThat((Map<String, String>) res.getResponseBody().get("foo")).containsEntry("bar", "A");
assertThat(res.getResponseBody()).containsEntry("quux", "C");
assertThat(res.getResponseBody()).doesNotContainKey("qux");
});
.uri("/anything/remove_json_attributes_recursively")
.exchange()
.expectStatus()
.isOk()
.expectBody(Map.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).containsKey("foo");
assertThat((Map<String, String>) res.getResponseBody().get("foo")).containsEntry("bar", "A");
assertThat(res.getResponseBody()).containsEntry("quux", "C");
assertThat(res.getResponseBody()).doesNotContainKey("qux");
});
}
@Test
void raisedErrorWhenRemoveJsonAttributes() {
restClient.get()
.uri("/anything/raised_error_when_remove_json_attributes")
.exchange()
.expectStatus()
.is5xxServerError()
.expectBody(String.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).isEqualTo("Failed to process JSON of response body.");
});
.uri("/anything/raised_error_when_remove_json_attributes")
.exchange()
.expectStatus()
.is5xxServerError()
.expectBody(String.class)
.consumeWith(res -> {
assertThat(res.getResponseBody()).isEqualTo("Failed to process JSON of response body.");
});
}
@SpringBootConfiguration

View File

@@ -91,13 +91,13 @@ public class GatewayPredicateVisitorTests {
PathRoutePredicateFactory pathRoutePredicateFactory = new PathRoutePredicateFactory(webFluxProperties);
PathRoutePredicateFactory.Config config = new PathRoutePredicateFactory.Config()
.setPatterns(List.of("/temp/**"))
.setMatchTrailingSlash(true);
.setPatterns(List.of("/temp/**"))
.setMatchTrailingSlash(true);
Predicate<ServerWebExchange> predicate = pathRoutePredicateFactory.apply(config);
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("http://127.0.0.1:8080/gw/api/v1/temp/test")
.build());
ServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("http://127.0.0.1:8080/gw/api/v1/temp/test").build());
assertThat(predicate.test(exchange)).isEqualTo(true);
}
@@ -109,13 +109,13 @@ public class GatewayPredicateVisitorTests {
PathRoutePredicateFactory pathRoutePredicateFactory = new PathRoutePredicateFactory(webFluxProperties);
PathRoutePredicateFactory.Config config = new PathRoutePredicateFactory.Config()
.setPatterns(List.of("/temp/**"))
.setMatchTrailingSlash(true);
.setPatterns(List.of("/temp/**"))
.setMatchTrailingSlash(true);
Predicate<ServerWebExchange> predicate = pathRoutePredicateFactory.apply(config);
ServerWebExchange exchange = MockServerWebExchange.from(MockServerHttpRequest.get("http://127.0.0.1:8080/gw/api/v1/temp/test")
.build());
ServerWebExchange exchange = MockServerWebExchange
.from(MockServerHttpRequest.get("http://127.0.0.1:8080/gw/api/v1/temp/test").build());
assertThat(predicate.test(exchange)).isEqualTo(true);
@@ -139,4 +139,5 @@ public class GatewayPredicateVisitorTests {
LinkedHashSet<URI> uris = webExchange.getRequiredAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
assertThat(uris).contains(exchange.getRequest().getURI());
}
}

View File

@@ -55,7 +55,8 @@ public class PathRoutePredicatePathContainerAttrBenchMarkTests {
PathRoutePredicateFactory.Config config = new PathRoutePredicateFactory.Config()
.setPatterns(Collections.singletonList(PATH_PATTERN_PREFIX + i))
.setMatchTrailingSlash(true);
Predicate<ServerWebExchange> predicate = new PathRoutePredicateFactory(new WebFluxProperties()).apply(config);
Predicate<ServerWebExchange> predicate = new PathRoutePredicateFactory(new WebFluxProperties())
.apply(config);
predicates.add(predicate);
}
}