diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java index 2aecabe1a8..cc69fd065a 100644 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java @@ -32,9 +32,9 @@ import org.springframework.boot.http.client.HttpRedirects; 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.FilterAutoConfiguration; import org.springframework.cloud.gateway.server.mvc.filter.FilterBeanFactoryDiscoverer; @@ -60,7 +60,6 @@ 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; @@ -80,7 +79,6 @@ PredicateAutoConfiguration.class }) @ConditionalOnProperty(name = GatewayMvcProperties.PREFIX + ".enabled", matchIfMissing = true) @Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class) -@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class) public class GatewayServerMvcAutoConfiguration { @Bean @@ -209,6 +207,11 @@ public XForwardedRequestHeadersFilterProperties xForwardedRequestHeadersFilterPr 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); diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java deleted file mode 100644 index c171c2403f..0000000000 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java +++ /dev/null @@ -1,83 +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.FilterAutoConfiguration; -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.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 { - - // TODO: fix AOT HINTS - private static final Set> FUNCTION_PROVIDERS = Set.of(HandlerFunctions.class, - FilterAutoConfiguration.LoadBalancerHandlerConfiguration.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> 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)); - } - -} diff --git a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java new file mode 100644 index 0000000000..4691a19f4d --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java @@ -0,0 +1,124 @@ +/* + * 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.cloud.gateway.server.mvc.filter.FilterAutoConfiguration; +import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration; +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> 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> PROPERTIES = Set.of(FilterProperties.class, PredicateProperties.class, + RouteProperties.class); + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + return (generationContext, beanFactoryInitializationCode) -> { + ReflectionHints hints = generationContext.getRuntimeHints().reflection(); + Set> 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> getTypesToRegister(String packageName) { + Set> classesToAdd = new HashSet<>(); + ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false); + provider.addIncludeFilter(new AssignableTypeFilter(Object.class)); + provider.addExcludeFilter(new AssignableTypeFilter(FilterAutoConfiguration.class)); + provider.addExcludeFilter(new AssignableTypeFilter(PredicateAutoConfiguration.class)); + Set 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 conditionClasses = beansConditionalOnClasses.getOrDefault(clazz.getName(), Collections.emptySet()); + for (String conditionClass : conditionClasses) { + try { + GatewayMvcRuntimeHintsProcessor.class.getClassLoader().loadClass(conditionClass); + } + catch (ClassNotFoundException e) { + return false; + } + } + return true; + } + +}