Browse Source

提交下,准备简化。

YunaiV 4 years ago
parent
commit
6bed7778e8
18 changed files with 726 additions and 24 deletions
  1. 7 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java
  2. 11 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/ConfigService.java
  3. 8 2
      src/main/java/cn/iocoder/dashboard/framework/apollox/DefaultConfig.java
  4. 79 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/internals/AbstractConfigRepository.java
  5. 35 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/internals/ConfigRepository.java
  6. 345 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RemoteConfigRepository.java
  7. 18 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/internals/RepositoryChangeListener.java
  8. 61 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloAnnotationProcessor.java
  9. 22 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfig.java
  10. 15 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/annotation/ApolloConfigChangeListener.java
  11. 30 7
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/boot/ApolloApplicationContextInitializer.java
  12. 11 1
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySource.java
  13. 31 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourceFactory.java
  14. 5 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/ConfigPropertySourcesProcessor.java
  15. 2 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/config/PropertySourcesConstants.java
  16. 19 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/property/PropertySourcesProcessor.java
  17. 14 0
      src/main/java/cn/iocoder/dashboard/framework/apollox/spring/util/SpringInjector.java
  18. 13 14
      src/main/java/cn/iocoder/dashboard/modules/system/controller/config/SysConfigController.java

+ 7 - 0
src/main/java/cn/iocoder/dashboard/framework/apollox/Config.java

@@ -25,4 +25,11 @@ public interface Config {
      */
     Set<String> getPropertyNames();
 
+    /**
+     * Add change listener to this config instance.
+     *
+     * @param listener the config change listener
+     */
+    void addChangeListener(ConfigChangeListener listener);
+
 }

+ 11 - 0
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);
+    }
+
+}

+ 8 - 2
src/main/java/cn/iocoder/dashboard/framework/apollox/DBConfig.java → 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<String> getPropertyNames() {
-        return null;
+        return Collections.emptySet(); // TODO 等下实现
+    }
+
+    @Override
+    public void addChangeListener(ConfigChangeListener listener) {
+
     }
 
 }

+ 79 - 0
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<RepositoryChangeListener> 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);
+            }
+        }
+    }
+
+}

+ 35 - 0
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.
+     * <p>
+     * 获得配置,以 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);
+
+}

+ 345 - 0
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
+ * <p>
+ * 远程配置 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<ApolloConfig> m_configCache;
+    /**
+     * Namespace 名字
+     */
+    private final String m_namespace;
+    /**
+     * ScheduledExecutorService 对象
+     */
+    private final static ScheduledExecutorService m_executorService;
+    /**
+     * 指向 ServiceDTO( Config Service 信息) 的 AtomicReference
+     */
+    private AtomicReference<ServiceDTO> m_longPollServiceDto;
+    /**
+     * 指向 ApolloNotificationMessages 的 AtomicReference
+     */
+    private AtomicReference<ApolloNotificationMessages> m_remoteMessages;
+    /**
+     * 加载配置的 RateLimiter
+     */
+    private RateLimiter m_loadConfigRateLimiter;
+    /**
+     * 是否强制拉取缓存的标记
+     * <p>
+     * 若为 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<ServiceDTO> configServices = getConfigServices();
+        String url = null;
+        // 循环读取配置重试次数直到成功。每一次,都会循环所有的 ServiceDTO 数组。
+        for (int i = 0; i < maxRetries; i++) {
+            // 随机所有的 Config Service 的地址
+            List<ServiceDTO> 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<ApolloConfig> 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<String> pathParams = Lists.newArrayList(pathEscaper.escape(appId), pathEscaper.escape(cluster), pathEscaper.escape(namespace));
+        Map<String, String> 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();
+            }
+
+        });
+    }
+
+
+}

+ 18 - 0
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);
+
+}

+ 61 - 0
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);
+    }
+
+}

+ 22 - 0
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.
+ *
+ * <p>Usage example:</p>
+ * <pre class="code">
+ * //Inject the config for "someNamespace"
+ * &#064;ApolloConfig("someNamespace")
+ * private Config config;
+ * </pre>
+ *
+ * @author Jason Song(song_s@ctrip.com)
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.FIELD)
+@Documented
+public @interface ApolloConfig {
+
+}

+ 15 - 0
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 {
+
+}

+ 30 - 7
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<ConfigurableApplicationContext> {
 
+    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<String> 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);
     }
 
 }

+ 11 - 1
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<Config> {
 
     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<Config> {
         return this.source.getProperty(name, null);
     }
 
+    /**
+     * 添加 ConfigChangeListener 到 Config 中
+     *
+     * @param listener 监听器
+     */
+    public void addChangeListener(ConfigChangeListener listener) {
+        this.source.addChangeListener(listener);
+    }
+
 }

+ 31 - 0
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<ConfigPropertySource> 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<ConfigPropertySource> getAllConfigPropertySources() {
+        return Lists.newLinkedList(configPropertySources);
+    }
+
+}

+ 5 - 0
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);

+ 2 - 0
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";
+
 }

+ 19 - 0
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<ConfigPropertySource> configPropertySources = configPropertySourceFactory.getAllConfigPropertySources();
+        for (ConfigPropertySource configPropertySource : configPropertySources) {
+            configPropertySource.addChangeListener(autoUpdateConfigChangeListener);
+        }
     }
 
     @Override

+ 14 - 0
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> T getInstance(Class<T> clazz) {
+        return Singleton.get(clazz);
+    }
+
+}

+ 13 - 14
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;