From d349a9f4cfdf96846ac7b4d301e610df2f3e55e0 Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Tue, 27 May 2025 17:05:29 +0200 Subject: [PATCH] Fix runtime hints for native images. (#3806) Signed-off-by: Olga Maciaszek-Sharma # Conflicts: # spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/GatewayServerMvcAutoConfiguration.java # spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java --- .../GatewayServerMvcAutoConfiguration.java | 9 +- .../GatewayMvcAotRuntimeHintsRegistrar.java | 82 ------------ .../GatewayMvcRuntimeHintsProcessor.java | 120 ++++++++++++++++++ 3 files changed, 126 insertions(+), 85 deletions(-) delete mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java create mode 100644 spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java 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 9baad4ce..b6807943 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 @@ -28,9 +28,9 @@ import org.springframework.boot.web.client.ClientHttpRequestFactories; import org.springframework.boot.web.client.ClientHttpRequestFactorySettings; 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; @@ -51,7 +51,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.Environment; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.JdkClientHttpRequestFactory; @@ -67,7 +66,6 @@ import org.springframework.web.client.RestClient; @AutoConfiguration(after = { RestTemplateAutoConfiguration.class, RestClientAutoConfiguration.class }) @ConditionalOnProperty(name = "spring.cloud.gateway.mvc.enabled", matchIfMissing = true) @Import(GatewayMvcPropertiesBeanDefinitionRegistrar.class) -@ImportRuntimeHints(GatewayMvcAotRuntimeHintsRegistrar.class) public class GatewayServerMvcAutoConfiguration { @Bean @@ -219,4 +217,9 @@ public class GatewayServerMvcAutoConfiguration { return new XForwardedRequestHeadersFilterProperties(); } + @Bean + static GatewayMvcRuntimeHintsProcessor gatewayMvcRuntimeHintsProcessor() { + return new GatewayMvcRuntimeHintsProcessor(); + } + } 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 e4482d3a..00000000 --- a/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcAotRuntimeHintsRegistrar.java +++ /dev/null @@ -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> 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> 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 00000000..fb78da79 --- /dev/null +++ b/spring-cloud-gateway-server-mvc/src/main/java/org/springframework/cloud/gateway/server/mvc/config/GatewayMvcRuntimeHintsProcessor.java @@ -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> 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)); + 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; + } + +}