From eb72d49506990730d0e4b2e358ba2540dab05c04 Mon Sep 17 00:00:00 2001 From: Haotian Zhang <928016560@qq.com> Date: Fri, 24 Jun 2022 15:19:44 +0800 Subject: [PATCH] Add config change listener feature support (#299) Co-authored-by: lepdou --- CHANGELOG.md | 1 + .../PolarisConfigAutoConfiguration.java | 12 + .../PolarisConfigAnnotationProcessor.java | 105 +++++++ .../PolarisConfigKVFileChangeListener.java | 58 ++++ .../config/listener/ConfigChangeEvent.java | 88 ++++++ .../config/listener/ConfigChangeListener.java | 35 +++ .../PolarisConfigChangeEventListener.java | 111 +++++++ .../PolarisConfigListenerContext.java | 277 ++++++++++++++++++ .../example/PersonConfigChangeListener.java | 49 ++++ 9 files changed, 736 insertions(+) create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigAnnotationProcessor.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigKVFileChangeListener.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeEvent.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListener.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigChangeEventListener.java create mode 100644 spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigListenerContext.java create mode 100644 spring-cloud-tencent-examples/polaris-config-example/src/main/java/com/tencent/cloud/polaris/config/example/PersonConfigChangeListener.java diff --git a/CHANGELOG.md b/CHANGELOG.md index e92d54042..9f272f41d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,3 +5,4 @@ - [feat:support reading configuration from application.yml or application.properties.](https://github.com/Tencent/spring-cloud-tencent/pull/262) - [fix:fix ClassNotFoundException while not importing openfeign when using circuit-breaker module.](https://github.com/Tencent/spring-cloud-tencent/pull/271) - [fix:solve ratelimit-callee-service UnknownHostException.](https://github.com/Tencent/spring-cloud-tencent/pull/292) +- [Feature: Add config change listener feature support](https://github.com/Tencent/spring-cloud-tencent/pull/299) diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java index 79255de5f..2b61b69be 100644 --- a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/PolarisConfigAutoConfiguration.java @@ -20,7 +20,9 @@ package com.tencent.cloud.polaris.config; import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceAutoRefresher; import com.tencent.cloud.polaris.config.adapter.PolarisPropertySourceManager; +import com.tencent.cloud.polaris.config.annotation.PolarisConfigAnnotationProcessor; import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.listener.PolarisConfigChangeEventListener; import com.tencent.cloud.polaris.context.ConditionalOnPolarisEnabled; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -46,4 +48,14 @@ public class PolarisConfigAutoConfiguration { contextRefresher); } + @Bean + public PolarisConfigAnnotationProcessor polarisConfigAnnotationProcessor() { + return new PolarisConfigAnnotationProcessor(); + } + + @Bean + public PolarisConfigChangeEventListener polarisConfigChangeEventListener() { + return new PolarisConfigChangeEventListener(); + } + } diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigAnnotationProcessor.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigAnnotationProcessor.java new file mode 100644 index 000000000..349799f69 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigAnnotationProcessor.java @@ -0,0 +1,105 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.annotation; + +import java.lang.reflect.Method; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import com.google.common.base.Preconditions; +import com.google.common.collect.Sets; +import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent; +import com.tencent.cloud.polaris.config.listener.ConfigChangeListener; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.lang.NonNull; +import org.springframework.util.ReflectionUtils; + +import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.addChangeListener; + +/** + * {@link PolarisConfigAnnotationProcessor} implementation for spring . + *

Refer to the Apollo project implementation: + * + * ApolloAnnotationProcessor + * @author Palmer Xu 2022-06-07 + */ +public class PolarisConfigAnnotationProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public Object postProcessBeforeInitialization(@NonNull Object bean, @NonNull String beanName) + throws BeansException { + Class clazz = bean.getClass(); + for (Method method : findAllMethod(clazz)) { + this.processPolarisConfigChangeListener(bean, method); + } + return bean; + } + + @Override + public Object postProcessAfterInitialization(@NonNull Object bean, @NonNull String beanName) throws BeansException { + return bean; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + private List findAllMethod(Class clazz) { + final List res = new LinkedList<>(); + ReflectionUtils.doWithMethods(clazz, res::add); + return res; + } + + private void processPolarisConfigChangeListener(final Object bean, final Method method) { + PolarisConfigKVFileChangeListener annotation = AnnotationUtils + .findAnnotation(method, PolarisConfigKVFileChangeListener.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); + + ReflectionUtils.makeAccessible(method); + String[] annotatedInterestedKeys = annotation.interestedKeys(); + String[] annotatedInterestedKeyPrefixes = annotation.interestedKeyPrefixes(); + + ConfigChangeListener configChangeListener = changeEvent -> ReflectionUtils.invokeMethod(method, bean, changeEvent); + + Set interestedKeys = + annotatedInterestedKeys.length > 0 ? Sets.newHashSet(annotatedInterestedKeys) : null; + Set interestedKeyPrefixes = + annotatedInterestedKeyPrefixes.length > 0 ? Sets.newHashSet(annotatedInterestedKeyPrefixes) + : null; + + addChangeListener(configChangeListener, interestedKeys, interestedKeyPrefixes); + } + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigKVFileChangeListener.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigKVFileChangeListener.java new file mode 100644 index 000000000..6acbf2336 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/annotation/PolarisConfigKVFileChangeListener.java @@ -0,0 +1,58 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Configuring the change listener annotation. + *

Refer to the Apollo project implementation: + * + * ApolloAnnotationProcessor + * @author Palmer Xu 2022-05-31 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +@Documented +public @interface PolarisConfigKVFileChangeListener { + + /** + * The keys interested in the listener, will only be notified if any of the interested keys is changed. + *
+ * If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when any key is changed. + * @return interested keys in the listener + */ + String[] interestedKeys() default {}; + + /** + * The key prefixes that the listener is interested in, will be notified if and only if the changed keys start with anyone of the prefixes. + * The prefixes will simply be used to determine whether the {@code listener} should be notified or not using {@code changedKey.startsWith(prefix)}. + * e.g. "spring." means that {@code listener} is interested in keys that starts with "spring.", such as "spring.banner", "spring.jpa", etc. + * and "application" means that {@code listener} is interested in keys that starts with "application", such as "applicationName", "application.port", etc. + *
+ * If neither of {@code interestedKeys} and {@code interestedKeyPrefixes} is specified then the {@code listener} will be notified when whatever key is changed. + * @return interested key-prefixed in the listener + */ + String[] interestedKeyPrefixes() default {}; + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeEvent.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeEvent.java new file mode 100644 index 000000000..119a5ce4c --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeEvent.java @@ -0,0 +1,88 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.listener; + +import java.util.Map; +import java.util.Set; + +import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; + +/** + * A change event when config is changed . + * + * @author Palmer Xu 2022-06-07 + */ +public final class ConfigChangeEvent { + + /** + * all changes keys map. + */ + private final Map changes; + + /** + * all interested changed keys. + */ + private final Set interestedChangedKeys; + + /** + * Config Change Event Constructor. + * @param changes all changes keys map + * @param interestedChangedKeys all interested changed keys + */ + public ConfigChangeEvent(Map changes, Set interestedChangedKeys) { + this.changes = changes; + this.interestedChangedKeys = interestedChangedKeys; + } + + /** + * Get the keys changed. + * @return the list of the keys + */ + public Set changedKeys() { + return changes.keySet(); + } + + /** + * Get a specific change instance for the key specified. + * @param key the changed key + * @return the change instance + */ + public ConfigPropertyChangeInfo getChange(String key) { + return changes.get(key); + } + + /** + * Check whether the specified key is changed . + * @param key the key + * @return true if the key is changed, false otherwise. + */ + public boolean isChanged(String key) { + return changes.containsKey(key); + } + + /** + * Maybe subclass override this method. + * + * @return interested and changed keys + */ + public Set interestedChangedKeys() { + return interestedChangedKeys; + } + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListener.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListener.java new file mode 100644 index 000000000..14ab64a32 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/ConfigChangeListener.java @@ -0,0 +1,35 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.listener; + +/** + * Configuring the change listener interface. + * + * @author Palmer Xu 2022-05-31 + */ +public interface ConfigChangeListener { + + /** + * Invoked when there is any config change for the namespace. + * + * @param changeEvent the event for this change + */ + void onChange(ConfigChangeEvent changeEvent); + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigChangeEventListener.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigChangeEventListener.java new file mode 100644 index 000000000..0a55255d5 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigChangeEventListener.java @@ -0,0 +1,111 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.listener; + +import java.util.Collection; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import com.google.common.collect.Maps; +import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; + +import org.springframework.boot.context.event.ApplicationStartedEvent; +import org.springframework.cloud.context.environment.EnvironmentChangeEvent; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; +import org.springframework.core.env.MutablePropertySources; +import org.springframework.lang.NonNull; + +import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.fireConfigChange; +import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.initialize; +import static com.tencent.cloud.polaris.config.listener.PolarisConfigListenerContext.merge; + +/** + * Polaris Config Change Event Listener . + * + * @author Elve.Xu 2022-06-08 + */ +public final class PolarisConfigChangeEventListener implements ApplicationListener { + + private static final AtomicBoolean started = new AtomicBoolean(); + + /** + * Handle an application event. + * + * @param event the event to respond to + */ + @Override + public void onApplicationEvent(@NonNull ApplicationEvent event) { + + // Initialize application all environment properties . + if (event instanceof ApplicationStartedEvent && started.compareAndSet(false, true)) { + ApplicationStartedEvent applicationStartedEvent = (ApplicationStartedEvent) event; + ConfigurableEnvironment environment = applicationStartedEvent.getApplicationContext().getEnvironment(); + Map ret = loadEnvironmentProperties(environment); + if (!ret.isEmpty()) { + initialize(ret); + } + } + + // Process Environment Change Event . + if (event instanceof EnvironmentChangeEvent) { + EnvironmentChangeEvent environmentChangeEvent = (EnvironmentChangeEvent) event; + ConfigurableApplicationContext context = (ConfigurableApplicationContext) environmentChangeEvent.getSource(); + ConfigurableEnvironment environment = context.getEnvironment(); + Map ret = loadEnvironmentProperties(environment); + Map changes = merge(ret); + fireConfigChange(changes.keySet(), Maps.newHashMap(changes)); + changes.clear(); + } + } + + /** + * Try load all application environment config properties . + * @param environment application environment instance of {@link Environment} + * @return properties + */ + @SuppressWarnings("unchecked") + private Map loadEnvironmentProperties(ConfigurableEnvironment environment) { + Map ret = Maps.newHashMap(); + MutablePropertySources sources = environment.getPropertySources(); + sources.iterator().forEachRemaining(propertySource -> { + Object o = propertySource.getSource(); + if (o instanceof Map) { + for (Map.Entry entry : ((Map) o).entrySet()) { + String key = entry.getKey(); + String value = environment.getProperty(key); + ret.put(key, value); + } + } + else if (o instanceof Collection) { + int count = 0; + Collection collection = (Collection) o; + for (Object object : collection) { + String key = "[" + (count++) + "]"; + ret.put(key, object); + } + } + }); + return ret; + } + +} diff --git a/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigListenerContext.java b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigListenerContext.java new file mode 100644 index 000000000..05ac41756 --- /dev/null +++ b/spring-cloud-starter-tencent-polaris-config/src/main/java/com/tencent/cloud/polaris/config/listener/PolarisConfigListenerContext.java @@ -0,0 +1,277 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.listener; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import com.tencent.polaris.configuration.api.core.ConfigKVFileChangeListener; +import com.tencent.polaris.configuration.api.core.ConfigPropertyChangeInfo; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.scheduling.concurrent.CustomizableThreadFactory; + +import static com.tencent.polaris.configuration.api.core.ChangeType.ADDED; +import static com.tencent.polaris.configuration.api.core.ChangeType.DELETED; +import static com.tencent.polaris.configuration.api.core.ChangeType.MODIFIED; + +/** + * Polaris Config Listener Context Defined . + *

Refer to the Apollo project implementation: + * + * AbstractConfig + * @author Palmer Xu 2022-06-06 + */ +public final class PolarisConfigListenerContext { + + /** + * Logger instance. + */ + private static final Logger LOG = LoggerFactory.getLogger(PolarisConfigListenerContext.class); + + /** + * Execute service Atomic Reference Cache . + */ + private static final AtomicReference EAR = new AtomicReference<>(); + + /** + * All custom {@link ConfigChangeListener} instance defined in application . + */ + private static final List listeners = Lists.newCopyOnWriteArrayList(); + + /** + * All custom interested keys defined in application . + */ + private static final Map> interestedKeys = Maps.newHashMap(); + + /** + * All custom interested key prefixes defined in application . + */ + private static final Map> interestedKeyPrefixes = Maps.newHashMap(); + + /** + * Cache all latest configuration information for users in the application environment . + */ + private static final Cache properties = CacheBuilder.newBuilder().build(); + + /** + * Get or Created new execute server . + * @return execute service instance of {@link ExecutorService} + */ + private static ExecutorService executor() { + if (EAR.get() == null) { + synchronized (PolarisConfigListenerContext.class) { + int coreThreadSize = Runtime.getRuntime().availableProcessors(); + final ExecutorService service = new ThreadPoolExecutor(coreThreadSize, coreThreadSize, + 0, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(64), + new CustomizableThreadFactory("Config-Change-Notify-Thread-Pool-")); + + // Register Jvm Shutdown Hook + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + try { + LOG.info("Shutting down config change notify thread pool"); + service.shutdown(); + } + catch (Exception ignore) { + } + })); + EAR.set(service); + } + } + return EAR.get(); + } + + /** + * Initialize Environment Properties cache after listen ApplicationStartedEvent event . + * @param ret origin properties map + */ + static void initialize(Map ret) { + properties.putAll(ret); + } + + /** + * Merge Changed Properties . + * @param ret current environment properties map + * @return merged properties result map + */ + static Map merge(Map ret) { + Map changes = Maps.newHashMap(); + if (!ret.isEmpty()) { + + Map origin = Maps.newHashMap(properties.asMap()); + Map deleted = Maps.newHashMap(); + + origin.keySet().parallelStream().forEach(key -> { + if (!ret.containsKey(key)) { + deleted.put(key, new ConfigPropertyChangeInfo(key, String.valueOf(origin.get(key)), null, DELETED)); + properties.invalidate(key); + } + }); + changes.putAll(deleted); + + ret.keySet().parallelStream().forEach(key -> { + Object oldValue = properties.getIfPresent(key); + Object newValue = ret.get(key); + if (oldValue != null) { + if (!newValue.equals(oldValue)) { + properties.put(key, newValue); + changes.put(key, new ConfigPropertyChangeInfo(key, String.valueOf(oldValue), String.valueOf(newValue), MODIFIED)); + } + } + else { + properties.put(key, newValue); + changes.put(key, new ConfigPropertyChangeInfo(key, null, String.valueOf(newValue), ADDED)); + } + }); + } + return changes; + } + + /** + * Adding a config file change listener, will trigger a callback when the config file is published . + * @param listener the listener will be added + * @param interestedKeys the keys interested in the listener, will only be notified if any of the interested keys is changed. + * @param interestedKeyPrefixes the key prefixes that the listener is interested in, + * will be notified if and only if the changed keys start with anyone of the prefixes. + */ + public static void addChangeListener(@NonNull ConfigChangeListener listener, + @Nullable Set interestedKeys, @Nullable Set interestedKeyPrefixes) { + if (!listeners.contains(listener)) { + listeners.add(listener); + PolarisConfigListenerContext.interestedKeys.put(listener, interestedKeys == null ? Sets.newHashSet() : interestedKeys); + PolarisConfigListenerContext.interestedKeyPrefixes.put(listener, interestedKeyPrefixes == null ? Sets.newHashSet() : interestedKeyPrefixes); + } + } + + /** + * Fire config change event with {@link ConfigKVFileChangeListener} . + * @param changedKeys changed keys in listener + * @param changes target config file changes info + */ + public static void fireConfigChange(Set changedKeys, Map changes) { + final List listeners = findMatchedConfigChangeListeners(changedKeys); + for (ConfigChangeListener listener : listeners) { + Set interestedChangedKeys = resolveInterestedChangedKeys(listener, changedKeys); + Map modifiedChanges = new HashMap<>(interestedChangedKeys.size()); + interestedChangedKeys.parallelStream().forEach(key -> modifiedChanges.put(key, changes.get(key))); + ConfigChangeEvent event = new ConfigChangeEvent(modifiedChanges, interestedChangedKeys); + PolarisConfigListenerContext.executor().execute(() -> listener.onChange(event)); + } + } + + /** + * Try to find all matched config change listeners . + * @param changedKeys received changed keys + * @return list of matched {@link ConfigChangeListener} + */ + private static List findMatchedConfigChangeListeners(Set changedKeys) { + final List configChangeListeners = Lists.newArrayList(); + for (ConfigChangeListener listener : listeners) { + if (isConfigChangeListenerInterested(listener, changedKeys)) { + configChangeListeners.add(listener); + } + } + return configChangeListeners; + } + + /** + * Check {@link ConfigChangeListener} is interested in custom keys. + * @param listener instance of {@link ConfigChangeListener} + * @param changedKeys received changed keys + * @return true is interested in custom keys + */ + private static boolean isConfigChangeListenerInterested(ConfigChangeListener listener, Set changedKeys) { + Set interestedKeys = PolarisConfigListenerContext.interestedKeys.get(listener); + Set interestedKeyPrefixes = PolarisConfigListenerContext.interestedKeyPrefixes.get(listener); + + if ((interestedKeys == null || interestedKeys.isEmpty()) + && (interestedKeyPrefixes == null || interestedKeyPrefixes.isEmpty())) { + return true; + } + + if (interestedKeys != null) { + for (String interestedKey : interestedKeys) { + if (changedKeys.contains(interestedKey)) { + return true; + } + } + } + + if (interestedKeyPrefixes != null) { + for (String prefix : interestedKeyPrefixes) { + for (final String changedKey : changedKeys) { + if (changedKey.startsWith(prefix)) { + return true; + } + } + } + } + return false; + } + + /** + * Resolve all interested keys . + * @param listener instance of {@link ConfigChangeListener} + * @param changedKeys received changed keys + * @return set of all interested keys in listener + */ + private static Set resolveInterestedChangedKeys(ConfigChangeListener listener, Set changedKeys) { + Set interestedChangedKeys = Sets.newHashSet(); + + if (interestedKeys.containsKey(listener)) { + Set interestedKeys = PolarisConfigListenerContext.interestedKeys.get(listener); + for (String interestedKey : interestedKeys) { + if (changedKeys.contains(interestedKey)) { + interestedChangedKeys.add(interestedKey); + } + } + } + + if (interestedKeyPrefixes.containsKey(listener)) { + Set interestedKeyPrefixes = PolarisConfigListenerContext.interestedKeyPrefixes.get(listener); + for (String interestedKeyPrefix : interestedKeyPrefixes) { + for (String changedKey : changedKeys) { + if (changedKey.startsWith(interestedKeyPrefix)) { + interestedChangedKeys.add(changedKey); + } + } + } + } + + return Collections.unmodifiableSet(interestedChangedKeys); + } + +} diff --git a/spring-cloud-tencent-examples/polaris-config-example/src/main/java/com/tencent/cloud/polaris/config/example/PersonConfigChangeListener.java b/spring-cloud-tencent-examples/polaris-config-example/src/main/java/com/tencent/cloud/polaris/config/example/PersonConfigChangeListener.java new file mode 100644 index 000000000..b0efd8b36 --- /dev/null +++ b/spring-cloud-tencent-examples/polaris-config-example/src/main/java/com/tencent/cloud/polaris/config/example/PersonConfigChangeListener.java @@ -0,0 +1,49 @@ +/* + * Tencent is pleased to support the open source community by making Spring Cloud Tencent available. + * + * Copyright (C) 2019 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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.tencent.cloud.polaris.config.example; + +import java.util.Set; + +import com.tencent.cloud.polaris.config.annotation.PolarisConfigKVFileChangeListener; +import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent; + +import org.springframework.stereotype.Component; + +/** + * Custom Config Listener Example . + * + * @author Palmer Xu 2022-06-06 + */ +@Component +public final class PersonConfigChangeListener { + + /** + * PolarisConfigKVFileChangeListener Example . + * @param event instance of {@link ConfigChangeEvent} + */ + @PolarisConfigKVFileChangeListener(interestedKeyPrefixes = "teacher") + public void onChange(ConfigChangeEvent event) { + Set changedKeys = event.changedKeys(); + + for (String changedKey : changedKeys) { + System.out.printf("%s = %s \n", changedKey, event.getChange(changedKey)); + } + } + +}