diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java index fbbddc853..188d34978 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java @@ -25,4 +25,11 @@ public interface Config { */ Set getPropertyNames(); + /** + * Add change listener to this config instance. + * + * @param listener the config change listener + */ + void addChangeListener(ConfigChangeListener listener); + } diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigService.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigService.java new file mode 100644 index 000000000..780b96101 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigService.java @@ -0,0 +1,11 @@ +package cn.iocoder.dashboard.framework.apollox; + +import cn.hutool.core.lang.Singleton; + +public class ConfigService { + + public static Config getConfig(String namespace) { + return Singleton.get(DefaultConfig.class); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/DefaultConfig.java similarity index 52% rename from src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java rename to src/main/java/cn/iocoder/dashboard/framework/apollox/DefaultConfig.java index 9ad3289ad..16fef1a15 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/DefaultConfig.java @@ -1,8 +1,9 @@ package cn.iocoder.dashboard.framework.apollox; +import java.util.Collections; import java.util.Set; -public class DBConfig implements Config { +public class DefaultConfig implements Config { @Override public String getProperty(String key, String defaultValue) { @@ -11,7 +12,12 @@ public class DBConfig implements Config { @Override public Set getPropertyNames() { - return null; + return Collections.emptySet(); // TODO 等下实现 + } + + @Override + public void addChangeListener(ConfigChangeListener listener) { + } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/AbstractConfigRepository.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/AbstractConfigRepository.java new file mode 100644 index 000000000..decfc7968 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/AbstractConfigRepository.java @@ -0,0 +1,79 @@ +package cn.iocoder.dashboard.framework.apollox.internals; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * 配置 Repository 抽象类 + * + * @author Jason Song(song_s@ctrip.com) + */ +public abstract class AbstractConfigRepository implements ConfigRepository { + + private static final Logger logger = LoggerFactory.getLogger(AbstractConfigRepository.class); + + /** + * RepositoryChangeListener 数组 + */ + private List m_listeners = new CopyOnWriteArrayList<>(); + + /** + * 尝试同步 + * + * @return 是否同步成功 + */ + protected boolean trySync() { + try { + // 同步 + sync(); + // 返回同步成功 + return true; + } catch (Throwable ex) { +// Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); + logger.warn("Sync config failed, will retry. Repository {}", getClass(), ex); + } + // 返回同步失败 + return false; + } + + /** + * 同步配置 + */ + protected abstract void sync(); + + @Override + public void addChangeListener(RepositoryChangeListener listener) { + if (!m_listeners.contains(listener)) { + m_listeners.add(listener); + } + } + + @Override + public void removeChangeListener(RepositoryChangeListener listener) { + m_listeners.remove(listener); + } + + /** + * 触发监听器们 + * + * @param namespace Namespace 名字 + * @param newProperties 配置 + */ + protected void fireRepositoryChange(String namespace, Properties newProperties) { + // 循环 RepositoryChangeListener 数组 + for (RepositoryChangeListener listener : m_listeners) { + try { + // 触发监听器 + listener.onRepositoryChange(namespace, newProperties); + } catch (Throwable ex) { +// Tracer.logError(ex); + logger.error("Failed to invoke repository change listener {}", listener.getClass(), ex); + } + } + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/ConfigRepository.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/ConfigRepository.java new file mode 100644 index 000000000..71e6357e9 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/ConfigRepository.java @@ -0,0 +1,35 @@ +package cn.iocoder.dashboard.framework.apollox.internals; + +import java.util.Properties; + +/** + * 配置 Repository 接口 + * + * @author Jason Song(song_s@ctrip.com) + */ +public interface ConfigRepository { + + /** + * Get the config from this repository. + *

+ * 获得配置,以 Properties 对象返回 + * + * @return config + */ + Properties getConfig(); + + /** + * Add change listener. + * + * @param listener the listener to observe the changes + */ + void addChangeListener(RepositoryChangeListener listener); + + /** + * Remove change listener. + * + * @param listener the listener to remove + */ + void removeChangeListener(RepositoryChangeListener listener); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RemoteConfigRepository.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RemoteConfigRepository.java new file mode 100644 index 000000000..0a95b96ce --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RemoteConfigRepository.java @@ -0,0 +1,345 @@ +package cn.iocoder.dashboard.framework.apollox.internals; + +import com.google.common.base.Joiner; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RemoteConfig Repository + *

+ * 远程配置 Repository ,实现从 Config Service 拉取配置,并缓存在内存中。并且,定时 + 实时刷新缓存。 + * + * @author Jason Song(song_s@ctrip.com) + */ +public class RemoteConfigRepository extends AbstractConfigRepository { + + private static final Logger logger = LoggerFactory.getLogger(RemoteConfigRepository.class); + private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR); + private static final Joiner.MapJoiner MAP_JOINER = Joiner.on("&").withKeyValueSeparator("="); + + private static final Escaper pathEscaper = UrlEscapers.urlPathSegmentEscaper(); + private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper(); + + /** + * 远程配置长轮询服务 + */ + private RemoteConfigLongPollService remoteConfigLongPollService; + /** + * 指向 ApolloConfig 的 AtomicReference ,缓存配置 + */ + private volatile AtomicReference m_configCache; + /** + * Namespace 名字 + */ + private final String m_namespace; + /** + * ScheduledExecutorService 对象 + */ + private final static ScheduledExecutorService m_executorService; + /** + * 指向 ServiceDTO( Config Service 信息) 的 AtomicReference + */ + private AtomicReference m_longPollServiceDto; + /** + * 指向 ApolloNotificationMessages 的 AtomicReference + */ + private AtomicReference m_remoteMessages; + /** + * 加载配置的 RateLimiter + */ + private RateLimiter m_loadConfigRateLimiter; + /** + * 是否强制拉取缓存的标记 + *

+ * 若为 true ,则多一轮从 Config Service 拉取配置 + * 为 true 的原因,RemoteConfigRepository 知道 Config Service 有配置刷新 + */ + private AtomicBoolean m_configNeedForceRefresh; + /** + * 失败定时重试策略,使用 {@link ExponentialSchedulePolicy} + */ + private SchedulePolicy m_loadConfigFailSchedulePolicy; + private Gson gson; + private ConfigUtil m_configUtil; + private HttpUtil m_httpUtil; + private ConfigServiceLocator m_serviceLocator; + + static { + // 单线程池 + m_executorService = Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("RemoteConfigRepository", true)); + } + + /** + * Constructor. + * + * @param namespace the namespace + */ + public RemoteConfigRepository(String namespace) { + m_namespace = namespace; + m_configCache = new AtomicReference<>(); + m_configUtil = ApolloInjector.getInstance(ConfigUtil.class); + m_httpUtil = ApolloInjector.getInstance(HttpUtil.class); + m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class); + remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class); + m_longPollServiceDto = new AtomicReference<>(); + m_remoteMessages = new AtomicReference<>(); + m_loadConfigRateLimiter = RateLimiter.create(m_configUtil.getLoadConfigQPS()); + m_configNeedForceRefresh = new AtomicBoolean(true); + m_loadConfigFailSchedulePolicy = new ExponentialSchedulePolicy(m_configUtil.getOnErrorRetryInterval(), m_configUtil.getOnErrorRetryInterval() * 8); + gson = new Gson(); + // 尝试同步配置 + super.trySync(); + // 初始化定时刷新配置的任务 + this.schedulePeriodicRefresh(); + // 注册自己到 RemoteConfigLongPollService 中,实现配置更新的实时通知 + this.scheduleLongPollingRefresh(); + } + + @Override + public Properties getConfig() { + // 如果缓存为空,强制从 Config Service 拉取配置 + if (m_configCache.get() == null) { + this.sync(); + } + // 转换成 Properties 对象,并返回 + return transformApolloConfigToProperties(m_configCache.get()); + } + + @Override + public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) { + // remote config doesn't need upstream + } + + private void schedulePeriodicRefresh() { + logger.debug("Schedule periodic refresh with interval: {} {}", m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); + // 创建定时任务,定时刷新配置 + m_executorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + // 【TODO 6001】Tracer 日志 + Tracer.logEvent("Apollo.ConfigService", String.format("periodicRefresh: %s", m_namespace)); + logger.debug("refresh config for namespace: {}", m_namespace); + // 尝试同步配置 + trySync(); + // 【TODO 6001】Tracer 日志 + Tracer.logEvent("Apollo.Client.Version", Apollo.VERSION); + } + }, m_configUtil.getRefreshInterval(), m_configUtil.getRefreshInterval(), m_configUtil.getRefreshIntervalTimeUnit()); + } + + @Override + protected synchronized void sync() { + // 【TODO 6001】Tracer 日志 + Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "syncRemoteConfig"); + try { + // 获得缓存的 ApolloConfig 对象 + ApolloConfig previous = m_configCache.get(); + // 从 Config Service 加载 ApolloConfig 对象 + ApolloConfig current = loadApolloConfig(); + + // reference equals means HTTP 304 + // 若不相等,说明更新了,设置到缓存中 + if (previous != current) { + logger.debug("Remote Config refreshed!"); + // 设置到缓存 + m_configCache.set(current); + // 发布 Repository 的配置发生变化,触发对应的监听器们 + super.fireRepositoryChange(m_namespace, this.getConfig()); + } + // 【TODO 6001】Tracer 日志 + if (current != null) { + Tracer.logEvent(String.format("Apollo.Client.Configs.%s", current.getNamespaceName()), current.getReleaseKey()); + } + // 【TODO 6001】Tracer 日志 + transaction.setStatus(Transaction.SUCCESS); + } catch (Throwable ex) { + // 【TODO 6001】Tracer 日志 + transaction.setStatus(ex); + throw ex; + } finally { + // 【TODO 6001】Tracer 日志 + transaction.complete(); + } + } + + private Properties transformApolloConfigToProperties(ApolloConfig apolloConfig) { + Properties result = new Properties(); + result.putAll(apolloConfig.getConfigurations()); + return result; + } + + private ApolloConfig loadApolloConfig() { + // 限流 + if (!m_loadConfigRateLimiter.tryAcquire(5, TimeUnit.SECONDS)) { + // wait at most 5 seconds + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + } + } + // 获得 appId cluster dataCenter 配置信息 + String appId = m_configUtil.getAppId(); + String cluster = m_configUtil.getCluster(); + String dataCenter = m_configUtil.getDataCenter(); + Tracer.logEvent("Apollo.Client.ConfigMeta", STRING_JOINER.join(appId, cluster, m_namespace)); + // 计算重试次数 + int maxRetries = m_configNeedForceRefresh.get() ? 2 : 1; + long onErrorSleepTime = 0; // 0 means no sleep + Throwable exception = null; + // 获得所有的 Config Service 的地址 + List configServices = getConfigServices(); + String url = null; + // 循环读取配置重试次数直到成功。每一次,都会循环所有的 ServiceDTO 数组。 + for (int i = 0; i < maxRetries; i++) { + // 随机所有的 Config Service 的地址 + List randomConfigServices = Lists.newLinkedList(configServices); + Collections.shuffle(randomConfigServices); + // 优先访问通知配置变更的 Config Service 的地址。并且,获取到时,需要置空,避免重复优先访问。 + // Access the server which notifies the client first + if (m_longPollServiceDto.get() != null) { + randomConfigServices.add(0, m_longPollServiceDto.getAndSet(null)); + } + // 循环所有的 Config Service 的地址 + for (ServiceDTO configService : randomConfigServices) { + // sleep 等待,下次从 Config Service 拉取配置 + if (onErrorSleepTime > 0) { + logger.warn("Load config failed, will retry in {} {}. appId: {}, cluster: {}, namespaces: {}", onErrorSleepTime, m_configUtil.getOnErrorRetryIntervalTimeUnit(), appId, cluster, m_namespace); + try { + m_configUtil.getOnErrorRetryIntervalTimeUnit().sleep(onErrorSleepTime); + } catch (InterruptedException e) { + //ignore + } + } + // 组装查询配置的地址 + url = assembleQueryConfigUrl(configService.getHomepageUrl(), appId, cluster, m_namespace, dataCenter, m_remoteMessages.get(), m_configCache.get()); + + logger.debug("Loading config from {}", url); + // 创建 HttpRequest 对象 + HttpRequest request = new HttpRequest(url); + + // 【TODO 6001】Tracer 日志 + Transaction transaction = Tracer.newTransaction("Apollo.ConfigService", "queryConfig"); + transaction.addData("Url", url); + try { + // 发起请求,返回 HttpResponse 对象 + HttpResponse response = m_httpUtil.doGet(request, ApolloConfig.class); + // 设置 m_configNeedForceRefresh = false + m_configNeedForceRefresh.set(false); + // 标记成功 + m_loadConfigFailSchedulePolicy.success(); + + // 【TODO 6001】Tracer 日志 + transaction.addData("StatusCode", response.getStatusCode()); + transaction.setStatus(Transaction.SUCCESS); + + // 无新的配置,直接返回缓存的 ApolloConfig 对象 + if (response.getStatusCode() == 304) { + logger.debug("Config server responds with 304 HTTP status code."); + return m_configCache.get(); + } + + // 有新的配置,进行返回新的 ApolloConfig 对象 + ApolloConfig result = response.getBody(); + logger.debug("Loaded config for {}: {}", m_namespace, result); + return result; + } catch (ApolloConfigStatusCodeException ex) { + ApolloConfigStatusCodeException statusCodeException = ex; + // 若返回的状态码是 404 ,说明查询配置的 Config Service 不存在该 Namespace 。 + // config not found + if (ex.getStatusCode() == 404) { + String message = String.format("Could not find config for namespace - appId: %s, cluster: %s, namespace: %s, " + + "please check whether the configs are released in Apollo!", appId, cluster, m_namespace); + statusCodeException = new ApolloConfigStatusCodeException(ex.getStatusCode(), message); + } + // 【TODO 6001】Tracer 日志 + Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(statusCodeException)); + transaction.setStatus(statusCodeException); + // 设置最终的异常 + exception = statusCodeException; + } catch (Throwable ex) { + // 【TODO 6001】Tracer 日志 + Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex)); + transaction.setStatus(ex); + // 设置最终的异常 + exception = ex; + } finally { + // 【TODO 6001】Tracer 日志 + transaction.complete(); + } + // 计算延迟时间 + // if force refresh, do normal sleep, if normal config load, do exponential sleep + onErrorSleepTime = m_configNeedForceRefresh.get() ? m_configUtil.getOnErrorRetryInterval() : m_loadConfigFailSchedulePolicy.fail(); + } + + } + // 若查询配置失败,抛出 ApolloConfigException 异常 + String message = String.format("Load Apollo Config failed - appId: %s, cluster: %s, namespace: %s, url: %s", appId, cluster, m_namespace, url); + throw new ApolloConfigException(message, exception); + } + + // 组装查询配置的地址 + String assembleQueryConfigUrl(String uri, String appId, String cluster, String namespace, + String dataCenter, ApolloNotificationMessages remoteMessages, ApolloConfig previousConfig) { + String path = "configs/%s/%s/%s"; // /configs/{appId}/{clusterName}/{namespace:.+} + List pathParams = Lists.newArrayList(pathEscaper.escape(appId), pathEscaper.escape(cluster), pathEscaper.escape(namespace)); + Map queryParams = Maps.newHashMap(); + // releaseKey + if (previousConfig != null) { + queryParams.put("releaseKey", queryParamEscaper.escape(previousConfig.getReleaseKey())); + } + // dataCenter + if (!Strings.isNullOrEmpty(dataCenter)) { + queryParams.put("dataCenter", queryParamEscaper.escape(dataCenter)); + } + // ip + String localIp = m_configUtil.getLocalIp(); + if (!Strings.isNullOrEmpty(localIp)) { + queryParams.put("ip", queryParamEscaper.escape(localIp)); + } + // messages + if (remoteMessages != null) { + queryParams.put("messages", queryParamEscaper.escape(gson.toJson(remoteMessages))); + } + // 格式化 URL + String pathExpanded = String.format(path, pathParams.toArray()); + // 拼接 Query String + if (!queryParams.isEmpty()) { + pathExpanded += "?" + MAP_JOINER.join(queryParams); + } + // 拼接最终的请求 URL + if (!uri.endsWith("/")) { + uri += "/"; + } + return uri + pathExpanded; + } + + /** + * 注册自己到 RemoteConfigLongPollService 中,实现配置更新的实时通知 + */ + private void scheduleLongPollingRefresh() { + remoteConfigLongPollService.submit(m_namespace, this); + } + + /** + * 当长轮询到配置更新时,发起同步配置的任务 + * + * @param longPollNotifiedServiceDto ServiceDTO 对象 + * @param remoteMessages ApolloNotificationMessages 对象 + */ + public void onLongPollNotified(ServiceDTO longPollNotifiedServiceDto, ApolloNotificationMessages remoteMessages) { + // 提交同步任务 + m_executorService.submit(new Runnable() { + + @Override + public void run() { + // 设置 m_configNeedForceRefresh 为 true + m_configNeedForceRefresh.set(true); + // 尝试同步配置 + trySync(); + } + + }); + } + + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RepositoryChangeListener.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RepositoryChangeListener.java new file mode 100644 index 000000000..338fae778 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RepositoryChangeListener.java @@ -0,0 +1,18 @@ +package cn.iocoder.dashboard.framework.apollox.internals; + +import java.util.Properties; + +/** + * @author Jason Song(song_s@ctrip.com) + */ +public interface RepositoryChangeListener { + + /** + * Invoked when config repository changes. + * + * @param namespace the namespace of this repository change + * @param newProperties the properties after change + */ + void onRepositoryChange(String namespace, Properties newProperties); + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloAnnotationProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloAnnotationProcessor.java new file mode 100644 index 000000000..b19305dfa --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloAnnotationProcessor.java @@ -0,0 +1,61 @@ +package cn.iocoder.dashboard.framework.apollox.spring.annotation; + +import cn.hutool.core.lang.Singleton; +import cn.iocoder.dashboard.framework.apollox.Config; +import cn.iocoder.dashboard.framework.apollox.ConfigChangeListener; +import cn.iocoder.dashboard.framework.apollox.DefaultConfig; +import cn.iocoder.dashboard.framework.apollox.model.ConfigChangeEvent; +import com.google.common.base.Preconditions; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * Apollo Annotation Processor for Spring Application + * + * @author Jason Song(song_s@ctrip.com) + */ +public class ApolloAnnotationProcessor extends ApolloProcessor { + + @Override + protected void processField(Object bean, String beanName, Field field) { + ApolloConfig annotation = AnnotationUtils.getAnnotation(field, ApolloConfig.class); + if (annotation == null) { + return; + } + + Preconditions.checkArgument(Config.class.isAssignableFrom(field.getType()), "Invalid type: %s for field: %s, should be Config", field.getType(), field); + + // 创建 Config 对象 + Config config = Singleton.get(DefaultConfig.class); + + // 设置 Config 对象,到对应的 Field + ReflectionUtils.makeAccessible(field); + ReflectionUtils.setField(field, bean, config); + } + + @Override + protected void processMethod(final Object bean, String beanName, final Method method) { + ApolloConfigChangeListener annotation = AnnotationUtils.findAnnotation(method, ApolloConfigChangeListener.class); + if (annotation == null) { + return; + } + Class[] parameterTypes = method.getParameterTypes(); + Preconditions.checkArgument(parameterTypes.length == 1, "Invalid number of parameters: %s for method: %s, should be 1", parameterTypes.length, method); + Preconditions.checkArgument(ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0]), "Invalid parameter type: %s for method: %s, should be ConfigChangeEvent", parameterTypes[0], method); + + // 创建 ConfigChangeListener 监听器。该监听器会调用被注解的方法。 + ReflectionUtils.makeAccessible(method); + ConfigChangeListener configChangeListener = changeEvent -> { + // 反射调用 + ReflectionUtils.invokeMethod(method, bean, changeEvent); + }; + + // 向指定 Namespace 的 Config 对象们,注册该监听器 + Config config = Singleton.get(DefaultConfig.class); + config.addChangeListener(configChangeListener); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfig.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfig.java new file mode 100644 index 000000000..476b5a236 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfig.java @@ -0,0 +1,22 @@ +package cn.iocoder.dashboard.framework.apollox.spring.annotation; + +import java.lang.annotation.*; + +/** + * Use this annotation to inject Apollo Config Instance. + * + *

Usage example:

+ *
+ * //Inject the config for "someNamespace"
+ * @ApolloConfig("someNamespace")
+ * private Config config;
+ * 
+ * + * @author Jason Song(song_s@ctrip.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Documented +public @interface ApolloConfig { + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfigChangeListener.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfigChangeListener.java new file mode 100644 index 000000000..7e642b3c5 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfigChangeListener.java @@ -0,0 +1,15 @@ +package cn.iocoder.dashboard.framework.apollox.spring.annotation; + +import java.lang.annotation.*; + +/** + * Use this annotation to register Apollo ConfigChangeListener. + * + * @author Jason Song(song_s@ctrip.com) + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface ApolloConfigChangeListener { + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java index ae88e77f9..f51c68735 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java @@ -1,20 +1,27 @@ package cn.iocoder.dashboard.framework.apollox.spring.boot; -import cn.hutool.core.lang.Singleton; import cn.iocoder.dashboard.framework.apollox.Config; -import cn.iocoder.dashboard.framework.apollox.DBConfig; -import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySource; +import cn.iocoder.dashboard.framework.apollox.ConfigService; +import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySourceFactory; +import cn.iocoder.dashboard.framework.apollox.spring.config.PropertySourcesConstants; +import cn.iocoder.dashboard.framework.apollox.spring.util.SpringInjector; import lombok.extern.slf4j.Slf4j; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; +import java.util.Collections; +import java.util.List; + import static cn.iocoder.dashboard.framework.apollox.spring.config.PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME; @Slf4j public class ApolloApplicationContextInitializer implements ApplicationContextInitializer { + private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector.getInstance(ConfigPropertySourceFactory.class); + @Override public void initialize(ConfigurableApplicationContext context) { ConfigurableEnvironment environment = context.getEnvironment(); @@ -24,11 +31,27 @@ public class ApolloApplicationContextInitializer implements ApplicationContextIn return; } - // 创建自定义的 APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME 的 PropertySource - Config config = Singleton.get(DBConfig.class); - ConfigPropertySource configPropertySource = new ConfigPropertySource(APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME, config); + // 忽略,若已经有 APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME 的 PropertySource + if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) { + // already initialized + return; + } + + // 获得 "apollo.bootstrap.namespaces" 配置项 + List namespaceList = Collections.singletonList("default"); + + // 按照优先级,顺序遍历 Namespace + CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME); + for (String namespace : namespaceList) { + // 创建 Apollo Config 对象 + Config config = ConfigService.getConfig(namespace); + // 创建 Namespace 对应的 ConfigPropertySource 对象 + // 添加到 `composite` 中。 + composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config)); + } + // 添加到 `environment` 中,且优先级最高 - environment.getPropertySources().addFirst(configPropertySource); + environment.getPropertySources().addFirst(composite); } } diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java index 8d0f4d41d..c4c6d9d7b 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java @@ -1,6 +1,7 @@ package cn.iocoder.dashboard.framework.apollox.spring.config; import cn.iocoder.dashboard.framework.apollox.Config; +import cn.iocoder.dashboard.framework.apollox.ConfigChangeListener; import org.springframework.core.env.EnumerablePropertySource; import java.util.Set; @@ -16,7 +17,7 @@ public class ConfigPropertySource extends EnumerablePropertySource { private static final String[] EMPTY_ARRAY = new String[0]; - public ConfigPropertySource(String name, Config source) { // 此处的 Apollo Config 作为 `source` + ConfigPropertySource(String name, Config source) { // 此处的 Apollo Config 作为 `source` super(name, source); } @@ -36,4 +37,13 @@ public class ConfigPropertySource extends EnumerablePropertySource { return this.source.getProperty(name, null); } + /** + * 添加 ConfigChangeListener 到 Config 中 + * + * @param listener 监听器 + */ + public void addChangeListener(ConfigChangeListener listener) { + this.source.addChangeListener(listener); + } + } diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourceFactory.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourceFactory.java new file mode 100644 index 000000000..418380b8a --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourceFactory.java @@ -0,0 +1,31 @@ +package cn.iocoder.dashboard.framework.apollox.spring.config; + +import cn.iocoder.dashboard.framework.apollox.Config; +import com.google.common.collect.Lists; + +import java.util.List; + +/** + * {@link ConfigPropertySource} 工厂 + */ +public class ConfigPropertySourceFactory { + + /** + * ConfigPropertySource 数组 + */ + private final List configPropertySources = Lists.newLinkedList(); + + // 创建 ConfigPropertySource 对象 + public ConfigPropertySource getConfigPropertySource(String name, Config source) { + // 创建 ConfigPropertySource 对象 + ConfigPropertySource configPropertySource = new ConfigPropertySource(name, source); + // 添加到数组中 + configPropertySources.add(configPropertySource); + return configPropertySource; + } + + public List getAllConfigPropertySources() { + return Lists.newLinkedList(configPropertySources); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java index bcf205648..218b681e4 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java @@ -1,5 +1,6 @@ package cn.iocoder.dashboard.framework.apollox.spring.config; +import cn.iocoder.dashboard.framework.apollox.spring.annotation.ApolloAnnotationProcessor; import cn.iocoder.dashboard.framework.apollox.spring.annotation.SpringValueProcessor; import cn.iocoder.dashboard.framework.apollox.spring.property.PropertySourcesProcessor; import cn.iocoder.dashboard.framework.apollox.spring.property.SpringValueDefinitionProcessor; @@ -20,8 +21,12 @@ public class ConfigPropertySourcesProcessor extends PropertySourcesProcessor imp public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { // 注册 PropertySourcesPlaceholderConfigurer 到 BeanDefinitionRegistry 中,替换 PlaceHolder 为对应的属性值,参考文章 https://leokongwq.github.io/2016/12/28/spring-PropertyPlaceholderConfigurer.html BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, PropertySourcesPlaceholderConfigurer.class.getName(), PropertySourcesPlaceholderConfigurer.class); + // 注册 ApolloAnnotationProcessor 到 BeanDefinitionRegistry 中,因为 XML 配置的 Bean 对象,也可能存在 @ApolloConfig 和 @ApolloConfigChangeListener 注解。 + BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloAnnotationProcessor.class.getName(), ApolloAnnotationProcessor.class); // 注册 SpringValueProcessor 到 BeanDefinitionRegistry 中,用于 PlaceHolder 自动更新机制 BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, SpringValueProcessor.class.getName(), SpringValueProcessor.class); + // 注册 ApolloJsonValueProcessor 到 BeanDefinitionRegistry 中,因为 XML 配置的 Bean 对象,也可能存在 @ApolloJsonValue 注解。 +// BeanRegistrationUtil.registerBeanDefinitionIfNotExists(registry, ApolloJsonValueProcessor.class.getName(), ApolloJsonValueProcessor.class); TODO 芋艿:暂时不需要迁移 // 处理 XML 配置的 Spring PlaceHolder processSpringValueDefinition(registry); diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java index 5cc048f97..9a4979077 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java @@ -4,4 +4,6 @@ public interface PropertySourcesConstants { String APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME = "ApolloBootstrapPropertySources"; + String APOLLO_PROPERTY_SOURCE_NAME = "ApolloPropertySources"; + } diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java index d3c7c77df..9e995cfe0 100644 --- a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java @@ -1,5 +1,8 @@ package cn.iocoder.dashboard.framework.apollox.spring.property; +import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySource; +import cn.iocoder.dashboard.framework.apollox.spring.config.ConfigPropertySourceFactory; +import cn.iocoder.dashboard.framework.apollox.spring.util.SpringInjector; import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -9,6 +12,7 @@ import org.springframework.core.PriorityOrdered; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -28,14 +32,29 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir */ private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false); + private final ConfigPropertySourceFactory configPropertySourceFactory = SpringInjector.getInstance(ConfigPropertySourceFactory.class); /** * Spring ConfigurableEnvironment 对象 */ private ConfigurableEnvironment environment; + @Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + if (INITIALIZED.compareAndSet(false, true)) { + // 初始化 AutoUpdateConfigChangeListener 对象,实现属性的自动更新 + initializeAutoUpdatePropertiesFeature(beanFactory); + } + } + private void initializeAutoUpdatePropertiesFeature(ConfigurableListableBeanFactory beanFactory) { + // 创建 AutoUpdateConfigChangeListener 对象 + AutoUpdateConfigChangeListener autoUpdateConfigChangeListener = new AutoUpdateConfigChangeListener(environment, beanFactory); + // 循环,向 ConfigPropertySource 注册配置变更器 + List configPropertySources = configPropertySourceFactory.getAllConfigPropertySources(); + for (ConfigPropertySource configPropertySource : configPropertySources) { + configPropertySource.addChangeListener(autoUpdateConfigChangeListener); + } } @Override diff --git a/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/SpringInjector.java b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/SpringInjector.java new file mode 100644 index 000000000..cb6d398b9 --- /dev/null +++ b/src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/SpringInjector.java @@ -0,0 +1,14 @@ +package cn.iocoder.dashboard.framework.apollox.spring.util; + +import cn.hutool.core.lang.Singleton; + +/** + * Spring 注入器 + */ +public class SpringInjector { + + public static T getInstance(Class clazz) { + return Singleton.get(clazz); + } + +} diff --git a/src/main/java/cn/iocoder/dashboard/modules/system/controller/config/SysConfigController.java b/src/main/java/cn/iocoder/dashboard/modules/system/controller/config/SysConfigController.java index 13189eaa3..7fdc7554f 100644 --- a/src/main/java/cn/iocoder/dashboard/modules/system/controller/config/SysConfigController.java +++ b/src/main/java/cn/iocoder/dashboard/modules/system/controller/config/SysConfigController.java @@ -11,6 +11,7 @@ import cn.iocoder.dashboard.modules.system.service.config.SysConfigService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiImplicitParam; import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Value; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -27,20 +28,18 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.CO @RequestMapping("/system/config") public class SysConfigController { -// private SpringValueRegistry -// -// @Value("demo.test") -// private String demo; -// -// @GetMapping("/demo") -// public String demo() { -// return demo; -// } -// -// @PostMapping("/demo") -// public void setDemo() { -// -// } + @Value("${demo.test:false}") + private String demo; + + @GetMapping("/demo") + public String demo() { + return demo; + } + + @PostMapping("/demo") + public void setDemo() { + + } @Resource private SysConfigService configService;