diff --git a/CHANGES.md b/CHANGES.md index 718d9634..2060a6a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -9,6 +9,7 @@ Apollo Java 2.5.0 * [Feature Provide a new open APl to return the organization list](https://github.com/apolloconfig/apollo-java/pull/102) * [Feature Added a new feature to get instance count by namespace.](https://github.com/apolloconfig/apollo-java/pull/103) * [Feature Support retry in open api client.](https://github.com/apolloconfig/apollo-java/pull/105) +* [Support Spring Boot 4.0 bootstrap context package relocation for apollo-client-config-data](https://github.com/apolloconfig/apollo-java/pull/115) ------------------ All issues and pull requests are [here](https://github.com/apolloconfig/apollo-java/milestone/5?closed=1) diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactory.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactory.java index 1405c3e3..1518aec8 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactory.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/initialize/ApolloClientExtensionInitializeFactory.java @@ -23,7 +23,6 @@ import com.ctrip.framework.apollo.config.data.extension.websocket.ApolloClientWebsocketExtensionInitializer; import com.ctrip.framework.apollo.config.data.util.Slf4jLogMessageFormatter; import org.apache.commons.logging.Log; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLogFactory; @@ -42,7 +41,7 @@ public class ApolloClientExtensionInitializeFactory { private final ApolloClientWebsocketExtensionInitializer apolloClientWebsocketExtensionInitializer; public ApolloClientExtensionInitializeFactory(DeferredLogFactory logFactory, - ConfigurableBootstrapContext bootstrapContext) { + Object bootstrapContext) { this.log = logFactory.getLog(ApolloClientExtensionInitializeFactory.class); this.apolloClientPropertiesFactory = new ApolloClientPropertiesFactory(); this.apolloClientLongPollingExtensionInitializer = new ApolloClientLongPollingExtensionInitializer( diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java index 337ae6ad..03185e56 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/ApolloClientLongPollingExtensionInitializer.java @@ -24,7 +24,6 @@ import com.ctrip.framework.foundation.internals.ServiceBootstrap; import java.util.List; import org.apache.commons.logging.Log; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLogFactory; @@ -40,10 +39,10 @@ public class ApolloClientLongPollingExtensionInitializer implements private final Log log; - private final ConfigurableBootstrapContext bootstrapContext; + private final Object bootstrapContext; public ApolloClientLongPollingExtensionInitializer(DeferredLogFactory logFactory, - ConfigurableBootstrapContext bootstrapContext) { + Object bootstrapContext) { this.log = logFactory.getLog(ApolloClientLongPollingExtensionInitializer.class); this.bootstrapContext = bootstrapContext; } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java index 8e3448ed..f7feb1ff 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/webclient/customizer/spi/ApolloClientWebClientCustomizerFactory.java @@ -19,7 +19,6 @@ import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties; import com.ctrip.framework.apollo.core.spi.Ordered; import org.apache.commons.logging.Log; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; @@ -37,11 +36,15 @@ public interface ApolloClientWebClientCustomizerFactory extends Ordered { * @param binder properties binder * @param bindHandler properties binder Handler * @param log deferred log - * @param bootstrapContext bootstrapContext + * @param bootstrapContext bootstrapContext (can be either + * org.springframework.boot.ConfigurableBootstrapContext for + * Spring Boot 3.x or + * org.springframework.boot.bootstrap.ConfigurableBootstrapContext + * for Spring Boot 4.x) * @return WebClientCustomizer instance or null */ @Nullable WebClientCustomizer createWebClientCustomizer(ApolloClientProperties apolloClientProperties, Binder binder, BindHandler bindHandler, Log log, - ConfigurableBootstrapContext bootstrapContext); + Object bootstrapContext); } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/websocket/ApolloClientWebsocketExtensionInitializer.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/websocket/ApolloClientWebsocketExtensionInitializer.java index 17d1dca5..35a8f7ed 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/websocket/ApolloClientWebsocketExtensionInitializer.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/extension/websocket/ApolloClientWebsocketExtensionInitializer.java @@ -19,7 +19,6 @@ import com.ctrip.framework.apollo.config.data.extension.initialize.ApolloClientExtensionInitializer; import com.ctrip.framework.apollo.config.data.extension.properties.ApolloClientProperties; import org.apache.commons.logging.Log; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Binder; import org.springframework.boot.logging.DeferredLogFactory; @@ -31,10 +30,10 @@ public class ApolloClientWebsocketExtensionInitializer implements ApolloClientEx private final Log log; - private final ConfigurableBootstrapContext bootstrapContext; + private final Object bootstrapContext; public ApolloClientWebsocketExtensionInitializer(DeferredLogFactory logFactory, - ConfigurableBootstrapContext bootstrapContext) { + Object bootstrapContext) { this.log = logFactory.getLog(ApolloClientWebsocketExtensionInitializer.class); this.bootstrapContext = bootstrapContext; } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoader.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoader.java index 8e15007b..41c84b64 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoader.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoader.java @@ -18,6 +18,7 @@ import com.ctrip.framework.apollo.Config; import com.ctrip.framework.apollo.ConfigService; +import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper; import com.ctrip.framework.apollo.config.data.util.Slf4jLogMessageFormatter; import com.ctrip.framework.apollo.spring.config.ConfigPropertySource; import com.ctrip.framework.apollo.spring.config.ConfigPropertySourceFactory; @@ -26,8 +27,6 @@ import java.util.ArrayList; import java.util.List; import org.apache.commons.logging.Log; -import org.springframework.boot.BootstrapRegistry.InstanceSupplier; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.config.ConfigData; import org.springframework.boot.context.config.ConfigDataLoader; import org.springframework.boot.context.config.ConfigDataLoaderContext; @@ -55,22 +54,24 @@ public ApolloConfigDataLoader(DeferredLogFactory logFactory) { @Override public ConfigData load(ConfigDataLoaderContext context, ApolloConfigDataResource resource) throws IOException, ConfigDataResourceNotFoundException { - ConfigurableBootstrapContext bootstrapContext = context.getBootstrapContext(); - Binder binder = bootstrapContext.get(Binder.class); + Object bootstrapContext = BootstrapRegistryHelper.getBootstrapContext(context); + Binder binder = BootstrapRegistryHelper.get(bootstrapContext, Binder.class); BindHandler bindHandler = this.getBindHandler(context); - bootstrapContext.registerIfAbsent(ApolloConfigDataLoaderInitializer.class, InstanceSupplier - .from(() -> new ApolloConfigDataLoaderInitializer(this.logFactory, binder, bindHandler, - bootstrapContext))); - ApolloConfigDataLoaderInitializer apolloConfigDataLoaderInitializer = bootstrapContext - .get(ApolloConfigDataLoaderInitializer.class); + BootstrapRegistryHelper.registerIfAbsentFromSupplier(bootstrapContext, + ApolloConfigDataLoaderInitializer.class, + () -> new ApolloConfigDataLoaderInitializer(this.logFactory, binder, bindHandler, + bootstrapContext)); + ApolloConfigDataLoaderInitializer apolloConfigDataLoaderInitializer = + BootstrapRegistryHelper.get(bootstrapContext, ApolloConfigDataLoaderInitializer.class); // init apollo client List> initialPropertySourceList = apolloConfigDataLoaderInitializer .initApolloClient(); // load config - bootstrapContext.registerIfAbsent(ConfigPropertySourceFactory.class, - InstanceSupplier.from(() -> SpringInjector.getInstance(ConfigPropertySourceFactory.class))); - ConfigPropertySourceFactory configPropertySourceFactory = bootstrapContext - .get(ConfigPropertySourceFactory.class); + BootstrapRegistryHelper.registerIfAbsentFromSupplier(bootstrapContext, + ConfigPropertySourceFactory.class, + () -> SpringInjector.getInstance(ConfigPropertySourceFactory.class)); + ConfigPropertySourceFactory configPropertySourceFactory = + BootstrapRegistryHelper.get(bootstrapContext, ConfigPropertySourceFactory.class); String namespace = resource.getNamespace(); Config config = ConfigService.getConfig(namespace); ConfigPropertySource configPropertySource = configPropertySourceFactory @@ -83,7 +84,8 @@ public ConfigData load(ConfigDataLoaderContext context, ApolloConfigDataResource } private BindHandler getBindHandler(ConfigDataLoaderContext context) { - return context.getBootstrapContext().getOrElse(BindHandler.class, null); + Object bootstrapContext = BootstrapRegistryHelper.getBootstrapContext(context); + return BootstrapRegistryHelper.getOrElse(bootstrapContext, BindHandler.class, null); } @Override diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializer.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializer.java index f85e71fb..22f6acbb 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializer.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/importer/ApolloConfigDataLoaderInitializer.java @@ -30,7 +30,6 @@ import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; -import org.springframework.boot.ConfigurableBootstrapContext; import org.springframework.boot.context.properties.bind.BindHandler; import org.springframework.boot.context.properties.bind.Bindable; import org.springframework.boot.context.properties.bind.Binder; @@ -54,11 +53,11 @@ class ApolloConfigDataLoaderInitializer { private final BindHandler bindHandler; - private final ConfigurableBootstrapContext bootstrapContext; + private final Object bootstrapContext; public ApolloConfigDataLoaderInitializer(DeferredLogFactory logFactory, Binder binder, BindHandler bindHandler, - ConfigurableBootstrapContext bootstrapContext) { + Object bootstrapContext) { this.logFactory = logFactory; this.log = logFactory.getLog(ApolloConfigDataLoaderInitializer.class); this.binder = binder; diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListener.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListener.java index 69ec5770..9578fc8c 100644 --- a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListener.java +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/listener/ApolloSpringApplicationRegisterListener.java @@ -16,8 +16,7 @@ */ package com.ctrip.framework.apollo.config.data.listener; -import org.springframework.boot.BootstrapRegistry.InstanceSupplier; -import org.springframework.boot.ConfigurableBootstrapContext; +import com.ctrip.framework.apollo.config.data.util.BootstrapRegistryHelper; import org.springframework.boot.SpringApplication; import org.springframework.boot.context.event.ApplicationStartingEvent; import org.springframework.context.ApplicationListener; @@ -30,8 +29,8 @@ public class ApolloSpringApplicationRegisterListener implements @Override public void onApplicationEvent(ApplicationStartingEvent event) { - ConfigurableBootstrapContext bootstrapContext = event.getBootstrapContext(); - bootstrapContext.registerIfAbsent(SpringApplication.class, - InstanceSupplier.of(event.getSpringApplication())); + Object bootstrapContext = BootstrapRegistryHelper.getBootstrapContext(event); + BootstrapRegistryHelper.registerIfAbsent(bootstrapContext, SpringApplication.class, + event.getSpringApplication()); } } diff --git a/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelper.java b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelper.java new file mode 100644 index 00000000..bc0abb85 --- /dev/null +++ b/apollo-client-config-data/src/main/java/com/ctrip/framework/apollo/config/data/util/BootstrapRegistryHelper.java @@ -0,0 +1,214 @@ +/* + * Copyright 2022 Apollo 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 + * + * http://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 com.ctrip.framework.apollo.config.data.util; + +import java.lang.reflect.Method; +import java.util.function.Supplier; +import org.springframework.boot.context.config.ConfigDataLoaderContext; +import org.springframework.boot.context.event.ApplicationStartingEvent; +import org.springframework.util.ClassUtils; + +/** + * Helper class to provide compatibility between Spring Boot 3.x and 4.x for bootstrap context + * operations. + *

+ * In Spring Boot 4.0, the bootstrap-related classes were moved from + * {@code org.springframework.boot} to {@code org.springframework.boot.bootstrap}. + * + * @author vdisk + */ +public class BootstrapRegistryHelper { + + private static final boolean SPRING_BOOT_4_PRESENT; + private static final Method GET_BOOTSTRAP_CONTEXT_FROM_EVENT_METHOD; + private static final Method GET_BOOTSTRAP_CONTEXT_FROM_LOADER_CONTEXT_METHOD; + private static final Method REGISTER_IF_ABSENT_METHOD; + private static final Method INSTANCE_SUPPLIER_OF_METHOD; + private static final Method INSTANCE_SUPPLIER_FROM_METHOD; + private static final Method GET_METHOD; + private static final Method GET_OR_ELSE_METHOD; + + static { + ClassLoader classLoader = BootstrapRegistryHelper.class.getClassLoader(); + SPRING_BOOT_4_PRESENT = ClassUtils.isPresent( + "org.springframework.boot.bootstrap.ConfigurableBootstrapContext", classLoader); + + try { + GET_BOOTSTRAP_CONTEXT_FROM_EVENT_METHOD = ApplicationStartingEvent.class.getMethod("getBootstrapContext"); + GET_BOOTSTRAP_CONTEXT_FROM_LOADER_CONTEXT_METHOD = ConfigDataLoaderContext.class.getMethod("getBootstrapContext"); + + Class bootstrapRegistryClass; + Class bootstrapContextClass; + String bootstrapPackage = SPRING_BOOT_4_PRESENT + ? "org.springframework.boot.bootstrap" + : "org.springframework.boot"; + bootstrapRegistryClass = ClassUtils.forName(bootstrapPackage + ".BootstrapRegistry", classLoader); + bootstrapContextClass = ClassUtils.forName(bootstrapPackage + ".BootstrapContext", classLoader); + + Class instanceSupplierClass = findInnerClass(bootstrapRegistryClass, "InstanceSupplier"); + REGISTER_IF_ABSENT_METHOD = bootstrapRegistryClass.getMethod("registerIfAbsent", + Class.class, instanceSupplierClass); + INSTANCE_SUPPLIER_OF_METHOD = instanceSupplierClass.getMethod("of", Object.class); + INSTANCE_SUPPLIER_FROM_METHOD = instanceSupplierClass.getMethod("from", Supplier.class); + GET_METHOD = bootstrapContextClass.getMethod("get", Class.class); + GET_OR_ELSE_METHOD = bootstrapContextClass.getMethod("getOrElse", Class.class, Object.class); + } catch (ClassNotFoundException e) { + throw new IllegalStateException( + "Failed to initialize BootstrapRegistryHelper: Bootstrap classes not found. " + + "Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } catch (NoSuchMethodException e) { + throw new IllegalStateException( + "Failed to initialize BootstrapRegistryHelper: Required method not found. " + + "Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } catch (Exception e) { + throw new IllegalStateException( + "Failed to initialize BootstrapRegistryHelper: Unexpected error during reflection setup. " + + "Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + private static Class findInnerClass(Class outerClass, String innerClassName) { + for (Class innerClass : outerClass.getDeclaredClasses()) { + if (innerClass.getSimpleName().equals(innerClassName)) { + return innerClass; + } + } + throw new IllegalStateException( + "Cannot find inner class " + innerClassName + " in " + outerClass.getName()); + } + + /** + * Get the bootstrap context from an ApplicationStartingEvent using reflection to support both + * Spring Boot 3.x and 4.x. + * + * @param event the ApplicationStartingEvent + * @return the bootstrap context (either from old or new package) + */ + public static Object getBootstrapContext(ApplicationStartingEvent event) { + try { + return GET_BOOTSTRAP_CONTEXT_FROM_EVENT_METHOD.invoke(event); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke ApplicationStartingEvent.getBootstrapContext() via reflection. " + + "Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Get the bootstrap context from a ConfigDataLoaderContext using reflection to support both + * Spring Boot 3.x and 4.x. + * + * @param context the ConfigDataLoaderContext + * @return the bootstrap context (either from old or new package) + */ + public static Object getBootstrapContext(ConfigDataLoaderContext context) { + try { + return GET_BOOTSTRAP_CONTEXT_FROM_LOADER_CONTEXT_METHOD.invoke(context); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke ConfigDataLoaderContext.getBootstrapContext() via reflection. " + + "Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Register an instance if absent in the bootstrap context using reflection. + * + * @param bootstrapContext the bootstrap context + * @param type the type to register + * @param instance the instance to register + * @param the type parameter + */ + public static void registerIfAbsent(Object bootstrapContext, Class type, T instance) { + try { + Object wrappedSupplier = INSTANCE_SUPPLIER_OF_METHOD.invoke(null, instance); + REGISTER_IF_ABSENT_METHOD.invoke(bootstrapContext, type, wrappedSupplier); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke BootstrapRegistry.registerIfAbsent() for type " + type.getName() + + " via reflection. Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Register an instance supplier if absent in the bootstrap context using reflection. + * + * @param bootstrapContext the bootstrap context + * @param type the type to register + * @param instanceSupplier supplier for the instance + * @param the type parameter + */ + public static void registerIfAbsentFromSupplier(Object bootstrapContext, Class type, + Supplier instanceSupplier) { + try { + Object wrappedSupplier = INSTANCE_SUPPLIER_FROM_METHOD.invoke(null, instanceSupplier); + REGISTER_IF_ABSENT_METHOD.invoke(bootstrapContext, type, wrappedSupplier); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke BootstrapRegistry.registerIfAbsent() with supplier for type " + + type.getName() + " via reflection. Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Get an instance from the bootstrap context using reflection. + * + * @param bootstrapContext the bootstrap context + * @param type the type to get + * @param the type parameter + * @return the instance + */ + @SuppressWarnings("unchecked") + public static T get(Object bootstrapContext, Class type) { + try { + return (T) GET_METHOD.invoke(bootstrapContext, type); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke BootstrapContext.get() for type " + type.getName() + + " via reflection. Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Get an instance from the bootstrap context or return a default value. + * + * @param bootstrapContext the bootstrap context + * @param type the type to get + * @param defaultValue the default value + * @param the type parameter + * @return the instance or default value + */ + @SuppressWarnings("unchecked") + public static T getOrElse(Object bootstrapContext, Class type, T defaultValue) { + try { + return (T) GET_OR_ELSE_METHOD.invoke(bootstrapContext, type, defaultValue); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException( + "Failed to invoke BootstrapContext.getOrElse() for type " + type.getName() + + " via reflection. Spring Boot 4.x detected: " + SPRING_BOOT_4_PRESENT, e); + } + } + + /** + * Check if Spring Boot 4.x is present. + * + * @return true if Spring Boot 4.x classes are available + */ + public static boolean isSpringBoot4Present() { + return SPRING_BOOT_4_PRESENT; + } +}