From d93217f108502657a8d4653d037a04236f233e75 Mon Sep 17 00:00:00 2001 From: "nov.lzf" Date: Thu, 26 Sep 2024 14:41:36 +0800 Subject: [PATCH] support nacos config annotation (#3856) --- .../nacos/NacosConfigAutoConfiguration.java | 9 +- .../AbstractConfigChangeListener.java | 73 +++ .../annotation/CustomDateDeserializer.java | 49 ++ .../cloud/nacos/annotation/JsonUtils.java | 66 ++ .../annotation/NacosAnnotationProcessor.java | 617 ++++++++++++++++++ .../cloud/nacos/annotation/NacosConfig.java | 42 ++ .../annotation/NacosConfigKeysListener.java | 38 ++ .../nacos/annotation/NacosConfigListener.java | 35 + .../NacosConfigRefreshableListener.java | 38 ++ .../NacosPropertiesKeyListener.java | 78 +++ .../cloud/nacos/annotation/ObjectUtils.java | 40 ++ .../nacos/annotation/PropertiesUtils.java | 74 +++ .../annotation/ScaYamlConfigChangeParser.java | 118 ++++ .../nacos/annotation/TargetRefreshable.java | 27 + ...cos.api.config.listener.ConfigChangeParser | 1 + 15 files changed, 1303 insertions(+), 2 deletions(-) create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/AbstractConfigChangeListener.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/CustomDateDeserializer.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/JsonUtils.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosAnnotationProcessor.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfig.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigKeysListener.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigListener.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigRefreshableListener.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosPropertiesKeyListener.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ObjectUtils.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/PropertiesUtils.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ScaYamlConfigChangeParser.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/TargetRefreshable.java create mode 100644 spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/resources/META-INF/services/com.alibaba.nacos.api.config.listener.ConfigChangeParser diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java index cd655ab08c..ac52c02c7f 100644 --- a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/NacosConfigAutoConfiguration.java @@ -16,6 +16,7 @@ package com.alibaba.cloud.nacos; +import com.alibaba.cloud.nacos.annotation.NacosAnnotationProcessor; import com.alibaba.cloud.nacos.refresh.NacosContextRefresher; import com.alibaba.cloud.nacos.refresh.NacosRefreshHistory; import com.alibaba.cloud.nacos.refresh.NacosRefreshProperties; @@ -68,8 +69,12 @@ public NacosConfigManager nacosConfigManager( } @Bean - public NacosContextRefresher nacosContextRefresher( - NacosConfigManager nacosConfigManager, + public NacosAnnotationProcessor nacosAnnotationProcessor() { + return new NacosAnnotationProcessor(); + } + + @Bean + public NacosContextRefresher nacosContextRefresher(NacosConfigManager nacosConfigManager, NacosRefreshHistory nacosRefreshHistory) { // Consider that it is not necessary to be compatible with the previous // configuration diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/AbstractConfigChangeListener.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/AbstractConfigChangeListener.java new file mode 100644 index 0000000000..8395f72716 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/AbstractConfigChangeListener.java @@ -0,0 +1,73 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.util.Map; + +import com.alibaba.nacos.api.config.ConfigChangeEvent; +import com.alibaba.nacos.api.config.ConfigChangeItem; +import com.alibaba.nacos.api.config.listener.AbstractSharedListener; +import com.alibaba.nacos.client.config.impl.ConfigChangeHandler; + +public abstract class AbstractConfigChangeListener extends AbstractSharedListener implements TargetRefreshable { + + String lastContent; + + Object target; + + @Override + public Object getTarget() { + return target; + } + + @Override + public void setTarget(Object target) { + this.target = target; + } + + public AbstractConfigChangeListener(Object target) { + this.target = target; + } + + protected void setLastContent(String lastContent) { + this.lastContent = lastContent; + } + + @Override + public void innerReceive(String dataId, String group, String configInfo) { + + Map data = null; + try { + data = ConfigChangeHandler.getInstance().parseChangeData(lastContent, configInfo, type(dataId)); + } + catch (Exception e) { + throw new RuntimeException(e); + } + ConfigChangeEvent event = new ConfigChangeEvent(data); + receiveConfigChange(event); + lastContent = configInfo; + } + + private String type(String dataId) { + if (dataId.endsWith(".yml") || dataId.endsWith(".yaml")) { + return "yaml"; + } + return "properties"; + } + + abstract void receiveConfigChange(ConfigChangeEvent event); +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/CustomDateDeserializer.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/CustomDateDeserializer.java new file mode 100644 index 0000000000..96af4f245b --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/CustomDateDeserializer.java @@ -0,0 +1,49 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; + +public class CustomDateDeserializer extends JsonDeserializer { + + private static final long serialVersionUID = 1L; + + private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + public CustomDateDeserializer() { + super(); + } + + @Override + public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException { + JsonNode node = jsonParser.getCodec().readTree(jsonParser); + String date = node.textValue(); + try { + return dateFormat.parse(date); + } + catch (Exception e) { + throw new IOException("Invalid date format"); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/JsonUtils.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/JsonUtils.java new file mode 100644 index 0000000000..329de98746 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/JsonUtils.java @@ -0,0 +1,66 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.io.IOException; +import java.lang.reflect.Type; + +import com.alibaba.nacos.api.exception.runtime.NacosDeserializationException; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.type.TypeFactory; + +final class JsonUtils { + + private JsonUtils() { + } + + static ObjectMapper mapper = new ObjectMapper(); + + static { + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + /** + * Json string deserialize to Object. + * + * @param json json string + * @param cls class of object + * @param General type + * @return object + * @throws NacosDeserializationException if deserialize failed + */ + public static T toObj(String json, Class cls) { + try { + return mapper.readValue(json, cls); + } + catch (IOException e) { + throw new NacosDeserializationException(cls, e); + } + } + + public static T toObj(String json, Type type) { + try { + return mapper.readValue(json, TypeFactory.defaultInstance().constructType(type)); + } + catch (IOException e) { + throw new NacosDeserializationException(type, e); + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosAnnotationProcessor.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosAnnotationProcessor.java new file mode 100644 index 0000000000..887a58cbd4 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosAnnotationProcessor.java @@ -0,0 +1,617 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicReference; + +import com.alibaba.cloud.nacos.NacosConfigManager; +import com.alibaba.nacos.api.config.ConfigChangeEvent; +import com.alibaba.nacos.api.config.ConfigChangeItem; +import com.alibaba.nacos.api.config.listener.AbstractListener; +import com.alibaba.nacos.client.config.common.GroupKey; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.core.PriorityOrdered; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ReflectionUtils; + +public class NacosAnnotationProcessor implements BeanPostProcessor, PriorityOrdered, ApplicationContextAware { + + private NacosConfigManager nacosConfigManager; + + private ApplicationContext applicationContext; + + private final static Logger log = LoggerFactory + .getLogger(NacosAnnotationProcessor.class); + + @Override + public int getOrder() { + return 0; + } + + private Map targetListenerMap = new ConcurrentHashMap<>(); + private Map> groupKeyCache = new ConcurrentHashMap<>(); + + private String getGroupKeyContent(String dataId, String group) throws Exception { + if (groupKeyCache.containsKey(GroupKey.getKey(dataId, group))) { + return groupKeyCache.get(GroupKey.getKey(dataId, group)).get(); + } + synchronized (this) { + if (!groupKeyCache.containsKey(GroupKey.getKey(dataId, group))) { + String content = nacosConfigManager.getConfigService().getConfig(dataId, group, 5000); + groupKeyCache.put(GroupKey.getKey(dataId, group), new AtomicReference<>(content)); + + log.info("[Nacos Config] Listening config for annotation: dataId={}, group={}", dataId, + group); + nacosConfigManager.getConfigService().addListener(dataId, group, new AbstractListener() { + @Override + public void receiveConfigInfo(String s) { + groupKeyCache.get(GroupKey.getKey(dataId, group)).set(s); + } + + @Override + public String toString() { + return String.format("sca nacos config annotation cache config listener"); + } + }); + + } + + return groupKeyCache.get(GroupKey.getKey(dataId, group)).get(); + } + + } + + @Override + public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName); + } + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + BeanPostProcessor.super.postProcessAfterInitialization(bean, beanName); + Class clazz = bean.getClass(); + for (Field field : getBeanFields(clazz)) { + handleFiledAnnotation(bean, beanName, field); + } + for (Method method : getBeanMethods(clazz)) { + handleMethodAnnotation(bean, beanName, method); + } + return bean; + } + + private List getBeanFields(Class clazz) { + List res = new ArrayList<>(); + ReflectionUtils.doWithFields(clazz, field -> res.add(field)); + return res; + } + + private List getBeanMethods(Class clazz) { + List res = new ArrayList<>(); + ReflectionUtils.doWithMethods(clazz, method -> res.add(method)); + return res; + } + + private void handleFiledAnnotation(Object bean, String beanName, Field field) { + NacosConfig annotation = AnnotationUtils.getAnnotation(field, NacosConfig.class); + if (annotation != null) { + handleFiledNacosConfigAnnotation(annotation, beanName, bean, field); + } + } + + private void handleMethodNacosConfigKeysChangeListener(NacosConfigKeysListener annotation, String beanName, Object bean, + Method method) { + String dataId = annotation.dataId(); + String group = annotation.group(); + try { + Class[] parameterTypes = method.getParameterTypes(); + if (parameterTypes.length != 1 || !ConfigChangeEvent.class.isAssignableFrom(parameterTypes[0])) { + throw new RuntimeException( + "NacosConfigKeysChangeListener must be marked as a single parameter with ConfigChangeEvent"); + } + + String refreshTargetKey = beanName + "#method#" + methodSignature(method); + TargetRefreshable currentTarget = targetListenerMap.get(refreshTargetKey); + if (currentTarget != null) { + log.info("[Nacos Config] reset {} listener from {} to {} ", refreshTargetKey, + currentTarget.getTarget(), bean); + targetListenerMap.get(refreshTargetKey).setTarget(bean); + return; + } + + log.info("[Nacos Config] register {} listener on {} ", refreshTargetKey, + bean); + // annotation on string. + NacosPropertiesKeyListener nacosPropertiesKeyListener = new NacosPropertiesKeyListener(bean, wrapArrayToSet(annotation.interestedKeys()), + wrapArrayToSet(annotation.interestedKeyPrefixes())) { + + @Override + public void configChanged(ConfigChangeEvent event) { + ReflectionUtils.invokeMethod(method, this.getTarget(), event); + } + + @Override + public String toString() { + return String.format("sca nacos config listener on bean method %s", bean + "#" + methodSignature(method)); + } + }; + nacosPropertiesKeyListener.setLastContent(getGroupKeyContent(dataId, group)); + nacosConfigManager.getConfigService().addListener(dataId, group, + nacosPropertiesKeyListener); + targetListenerMap.put(refreshTargetKey, nacosPropertiesKeyListener); + } + catch (Throwable e) { + throw new RuntimeException(e); + } + } + + private Set wrapArrayToSet(String... arrayKeys) { + return new HashSet<>(Arrays.asList(arrayKeys)); + } + + private String methodSignature(Method method) { + StringBuilder signature = new StringBuilder(method.getName() + "("); + Class[] parameterTypes = method.getParameterTypes(); + for (int i = 0; i < parameterTypes.length; i++) { + signature.append(parameterTypes[i].getSimpleName()); + if (i < parameterTypes.length - 1) { + signature.append(", "); + } + } + + signature.append(")"); + return signature.toString(); + } + + private void handleMethodNacosConfigListener(NacosConfigListener annotation, String beanName, Object bean, Method method) { + String dataId = annotation.dataId(); + String group = annotation.group(); + String key = annotation.key(); + try { + Type[] parameterTypes = method.getGenericParameterTypes(); + if (parameterTypes.length != 1) { + throw new RuntimeException( + "@NacosConfigListener must be over a method with a single parameter"); + } + + String configInfo = getGroupKeyContent(dataId, group); + String refreshTargetKey = beanName + "#method#" + methodSignature(method); + TargetRefreshable currentTarget = targetListenerMap.get(refreshTargetKey); + if (currentTarget != null) { + log.info("[Nacos Config] reset {} listener from {} to {} ", refreshTargetKey, + currentTarget.getTarget(), bean); + targetListenerMap.get(refreshTargetKey).setTarget(bean); + return; + } + + log.info("[Nacos Config] register {} listener on {} ", refreshTargetKey, + bean); + + TargetRefreshable listener = null; + if (org.springframework.util.StringUtils.hasText(key)) { + listener = new NacosPropertiesKeyListener(bean, wrapArrayToSet(key)) { + + @Override + public void configChanged(ConfigChangeEvent event) { + try { + ConfigChangeItem changeItem = event.getChangeItem(key); + String newConfig = changeItem == null ? null : changeItem.getNewValue(); + + if (org.springframework.util.StringUtils.hasText(newConfig)) { + if (invokePrimitiveMethod(method, getTarget(), newConfig)) { + return; + } + + Object targetObject = convertContentToTargetType(newConfig, parameterTypes[0]); + ReflectionUtils.invokeMethod(method, getTarget(), targetObject); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config key listener , key %s , target %s ] ", key, bean + "#" + methodSignature(method)); + } + }; + ((AbstractConfigChangeListener) listener).fillContext(dataId, group); + if (!annotation.initNotify()) { + ((AbstractConfigChangeListener) listener).setLastContent(configInfo); + } + } + else { + listener = new NacosConfigRefreshableListener(bean) { + + @Override + public void receiveConfigInfo(String configInfo) { + if (org.springframework.util.StringUtils.hasText(configInfo)) { + try { + if (invokePrimitiveMethod(method, getTarget(), configInfo)) { + return; + } + Object targetObject = convertContentToTargetType(configInfo, parameterTypes[0]); + ReflectionUtils.invokeMethod(method, getTarget(), targetObject); + } + catch (Exception e) { + throw new RuntimeException(e); + } + + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config listener , target %s ] ", bean + "#" + methodSignature(method)); + } + }; + } + + nacosConfigManager.getConfigService().addListener(dataId, group, listener); + targetListenerMap.put(refreshTargetKey, listener); + if (annotation.initNotify() && org.springframework.util.StringUtils.hasText(configInfo)) { + try { + log.info("[Nacos Config] init notify listener of {} on {} start...", refreshTargetKey, + bean); + listener.receiveConfigInfo(configInfo); + log.info("[Nacos Config] init notify listener of {} on {} finished ", refreshTargetKey, + bean); + } + catch (Throwable throwable) { + log.warn("[Nacos Config] init notify listener error", throwable); + throw throwable; + } + } + } + catch (Throwable e) { + throw new RuntimeException(e); + } + } + + + Object convertContentToTargetType(String rawContent, Type type) { + + if (String.class.getCanonicalName().equals(type.getTypeName())) { + return rawContent; + } + + if (Properties.class.getCanonicalName().equals(type.getTypeName())) { + //properties and yaml config to properties. + Properties properties = new Properties(); + try { + if (org.springframework.util.StringUtils.hasText(rawContent)) { + properties = PropertiesUtils.convertToProperties(rawContent); + } + } + catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + return properties; + } + return ObjectUtils.convertToObject(rawContent, type); + } + + private void handleFiledNacosConfigAnnotation(NacosConfig annotation, String beanName, Object bean, Field field) { + String dataId = annotation.dataId(); + String group = annotation.group(); + String key = annotation.key(); + try { + ReflectionUtils.makeAccessible(field); + handleFiledNacosConfigAnnotationInner(dataId, group, key, beanName, bean, field, annotation.defaultValue()); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void handleFiledNacosConfigAnnotationInner(String dataId, String group, String key, String beanName, Object bean, + Field field, String defaultValue) { + try { + String config = getDestContent(getGroupKeyContent(dataId, group), key); + if (!org.springframework.util.StringUtils.hasText(config)) { + config = defaultValue; + } + + //primitive type + if (handPrimitiveFiled(field, dataId, group, config, key, defaultValue, beanName, bean)) { + return; + } + + //for other type. + if (org.springframework.util.StringUtils.hasText(config)) { + Object targetObject = convertContentToTargetType(config, field.getGenericType()); + //yaml and json to object + ReflectionUtils.setField(field, bean, targetObject); + } + + String refreshTargetKey = beanName + "#filed#" + field.getName(); + TargetRefreshable currentTarget = targetListenerMap.get(refreshTargetKey); + if (currentTarget != null) { + log.info("[Nacos Config] reset {} listener from {} to {} ", refreshTargetKey, + currentTarget.getTarget(), bean); + targetListenerMap.get(refreshTargetKey).setTarget(bean); + return; + } + + log.info("[Nacos Config] register {} listener on {} ", refreshTargetKey, + bean); + TargetRefreshable listener = null; + if (org.springframework.util.StringUtils.hasText(key)) { + listener = new NacosPropertiesKeyListener(bean, wrapArrayToSet(key)) { + + @Override + public void configChanged(ConfigChangeEvent event) { + try { + ConfigChangeItem changeItem = event.getChangeItem(key); + String newConfig = changeItem == null ? null : changeItem.getNewValue(); + if (!org.springframework.util.StringUtils.hasText(newConfig)) { + newConfig = defaultValue; + } + if (org.springframework.util.StringUtils.hasText(newConfig)) { + Object targetObject = convertContentToTargetType(newConfig, field.getGenericType()); + ReflectionUtils.setField(field, getTarget(), targetObject); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config key listener , key %s , target %s ] ", key, bean + "#" + field.getName()); + } + }; + } + else { + listener = new NacosConfigRefreshableListener(bean) { + + @Override + public void receiveConfigInfo(String configInfo) { + if (!org.springframework.util.StringUtils.hasText(configInfo)) { + configInfo = defaultValue; + } + if (org.springframework.util.StringUtils.hasText(configInfo)) { + Object targetObject = convertContentToTargetType(configInfo, field.getGenericType()); + ReflectionUtils.setField(field, getTarget(), targetObject); + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config key listener , key %s , target %s ] ", key, bean + "#" + field.getName()); + } + }; + } + + nacosConfigManager.getConfigService() + .addListener(dataId, group, listener); + targetListenerMap.put(refreshTargetKey, listener); + + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + private boolean handPrimitiveFiled(Field field, String dataId, String group, String config, String key, String defaultValue, String beanName, Object bean) throws Exception { + if (field.getType().isPrimitive()) { + + if (org.springframework.util.StringUtils.hasText(config)) { + try { + setPrimitiveFiled(field, bean, config); + } + catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + } + + String refreshTargetKey = beanName + "#filed#" + field.getName(); + TargetRefreshable currentTarget = targetListenerMap.get(refreshTargetKey); + if (currentTarget != null) { + log.info("[Nacos Config] reset {} listener from {} to {} ", refreshTargetKey, + currentTarget.getTarget(), bean); + targetListenerMap.get(refreshTargetKey).setTarget(bean); + return true; + } + + log.info("[Nacos Config] register {} listener on {} ", refreshTargetKey, + bean); + TargetRefreshable listener = null; + if (org.springframework.util.StringUtils.hasText(key)) { + listener = new NacosPropertiesKeyListener(bean, wrapArrayToSet(key)) { + + @Override + public void configChanged(ConfigChangeEvent event) { + try { + ConfigChangeItem changeItem = event.getChangeItem(key); + String newConfig = changeItem == null ? null : changeItem.getNewValue(); + if (!org.springframework.util.StringUtils.hasText(newConfig)) { + newConfig = defaultValue; + } + if (org.springframework.util.StringUtils.hasText(newConfig)) { + setPrimitiveFiled(field, getTarget(), newConfig); + } + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config key listener , key %s , target %s ] ", key, bean + "#" + field.getName()); + } + }; + } + else { + listener = new NacosConfigRefreshableListener(bean) { + + @Override + public void receiveConfigInfo(String configInfo) { + if (!org.springframework.util.StringUtils.hasText(configInfo)) { + configInfo = defaultValue; + } + if (org.springframework.util.StringUtils.hasText(configInfo)) { + try { + setPrimitiveFiled(field, getTarget(), configInfo); + } + catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + @Override + public String toString() { + return String.format("[spring cloud alibaba nacos config key listener , key %s , target %s ] ", key, bean + "#" + field.getName()); + } + }; + } + + nacosConfigManager.getConfigService() + .addListener(dataId, group, listener); + targetListenerMap.put(refreshTargetKey, listener); + return true; + } + return false; + } + + private boolean setPrimitiveFiled(Field filed, Object bean, String value) throws Exception { + if (filed.getType() == int.class) { + filed.setInt(bean, Integer.parseInt(value)); + } + else if (filed.getType() == Integer.class) { + ReflectionUtils.setField(filed, bean, Integer.valueOf(value)); + } + else if (filed.getType() == long.class) { + filed.setLong(bean, Long.parseLong(value)); + } + else if (filed.getType() == Long.class) { + ReflectionUtils.setField(filed, bean, Long.valueOf(value)); + } + else if (filed.getType() == boolean.class) { + filed.setBoolean(bean, Boolean.parseBoolean(value)); + } + else if (filed.getType() == Boolean.class) { + ReflectionUtils.setField(filed, bean, Boolean.valueOf(value)); + } + else if (filed.getType() == double.class) { + filed.setDouble(bean, Double.parseDouble(value)); + } + else if (filed.getType() == Double.class) { + ReflectionUtils.setField(filed, bean, Double.valueOf(value)); + } + else if (filed.getType() == float.class) { + filed.setFloat(bean, Float.parseFloat(value)); + } + else if (filed.getType() == Float.class) { + ReflectionUtils.setField(filed, bean, Float.valueOf(value)); + } + else { + return false; + } + return true; + } + + private boolean invokePrimitiveMethod(Method method, Object bean, String value) throws Exception { + Class parameterType = method.getParameterTypes()[0]; + if (parameterType == int.class) { + ReflectionUtils.invokeMethod(method, bean, Integer.parseInt(value)); + } + else if (parameterType == Integer.class) { + ReflectionUtils.invokeMethod(method, bean, Integer.valueOf(value)); + } + else if (parameterType == long.class) { + ReflectionUtils.invokeMethod(method, bean, Long.parseLong(value)); + } + else if (parameterType == Long.class) { + ReflectionUtils.invokeMethod(method, bean, Long.valueOf(value)); + } + else if (parameterType == boolean.class) { + ReflectionUtils.invokeMethod(method, bean, Boolean.parseBoolean(value)); + } + else if (parameterType == Boolean.class) { + ReflectionUtils.invokeMethod(method, bean, Boolean.valueOf(value)); + } + else if (parameterType == double.class) { + ReflectionUtils.invokeMethod(method, bean, Double.parseDouble(value)); + } + else if (parameterType == Double.class) { + ReflectionUtils.invokeMethod(method, bean, Double.valueOf(value)); + } + else if (parameterType == float.class) { + ReflectionUtils.invokeMethod(method, bean, Float.parseFloat(value)); + } + else if (parameterType == Float.class) { + ReflectionUtils.invokeMethod(method, bean, Float.valueOf(value)); + } + else { + return false; + } + return true; + } + + private String getDestContent(String content, String key) throws Exception { + if (org.springframework.util.StringUtils.hasText(key)) { + Properties properties = PropertiesUtils.convertToProperties(content); + return properties.getProperty(key); + } + else { + return content; + } + } + + private void handleMethodAnnotation(final Object bean, String beanName, final Method method) { + NacosConfigKeysListener keysAnnotation = AnnotationUtils.getAnnotation(method, NacosConfigKeysListener.class); + if (keysAnnotation != null) { + ReflectionUtils.makeAccessible(method); + handleMethodNacosConfigKeysChangeListener(keysAnnotation, beanName, bean, method); + return; + } + NacosConfigListener configAnnotation = AnnotationUtils.getAnnotation(method, NacosConfigListener.class); + if (configAnnotation != null) { + ReflectionUtils.makeAccessible(method); + handleMethodNacosConfigListener(configAnnotation, beanName, bean, method); + return; + } + + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + nacosConfigManager = this.applicationContext.getBean(NacosConfigManager.class); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfig.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfig.java new file mode 100644 index 0000000000..1799803b47 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfig.java @@ -0,0 +1,42 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.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; + +/** + * Nacos Config annotation. + * + * @author shiyiyue1102 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.FIELD}) +@Documented +public @interface NacosConfig { + + String group(); + + String dataId(); + + String key() default ""; + + String defaultValue() default ""; +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigKeysListener.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigKeysListener.java new file mode 100644 index 0000000000..e5cf724922 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigKeysListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @ConfigChangeEvent + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface NacosConfigKeysListener { + + String dataId(); + + String group(); + + String[] interestedKeys() default {}; + + String[] interestedKeyPrefixes() default {}; +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigListener.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigListener.java new file mode 100644 index 0000000000..fae7087b85 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigListener.java @@ -0,0 +1,35 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +public @interface NacosConfigListener { + + String dataId(); + + String group(); + + String key() default ""; + + boolean initNotify() default false; +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigRefreshableListener.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigRefreshableListener.java new file mode 100644 index 0000000000..ff339e69bd --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosConfigRefreshableListener.java @@ -0,0 +1,38 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import com.alibaba.nacos.api.config.listener.AbstractListener; + +public abstract class NacosConfigRefreshableListener extends AbstractListener implements TargetRefreshable { + + Object target; + + NacosConfigRefreshableListener(Object target) { + this.target = target; + } + + public Object getTarget() { + return target; + } + + @Override + public void setTarget(Object target) { + this.target = target; + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosPropertiesKeyListener.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosPropertiesKeyListener.java new file mode 100644 index 0000000000..2e27db7f43 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/NacosPropertiesKeyListener.java @@ -0,0 +1,78 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.util.Set; + +import com.alibaba.nacos.api.config.ConfigChangeEvent; +import com.alibaba.nacos.api.config.ConfigChangeItem; +import com.alibaba.nacos.common.utils.CollectionUtils; + +public abstract class NacosPropertiesKeyListener extends AbstractConfigChangeListener { + + Set interestedKeys; + + Set interestedKeyPrefixes; + + NacosPropertiesKeyListener(Object target) { + super(target); + } + + NacosPropertiesKeyListener(Object target, Set interestedKeys) { + this(target); + this.interestedKeys = interestedKeys; + } + + public NacosPropertiesKeyListener(Object target, Set interestedKeys, Set interestedKeyPrefixes) { + this(target); + this.interestedKeys = interestedKeys; + this.interestedKeyPrefixes = interestedKeyPrefixes; + } + + @Override + public final void receiveConfigChange(ConfigChangeEvent event) { + if (CollectionUtils.isNotEmpty(interestedKeys) || CollectionUtils.isNotEmpty(interestedKeyPrefixes)) { + boolean foundInterested = false; + for (ConfigChangeItem changeItem : event.getChangeItems()) { + if (interestedKeys != null && interestedKeys.contains(changeItem.getKey())) { + foundInterested = true; + break; + } + if (interestedKeyPrefixes != null) { + for (String prefix : interestedKeyPrefixes) { + if (changeItem.getKey().startsWith(prefix)) { + foundInterested = true; + break; + } + } + } + } + if (!foundInterested) { + return; + } + } + configChanged(event); + } + + @Override + public String toString() { + return "NacosPropertiesKeyListener{" + "interestedKeys=" + interestedKeys + ", interestedKeyPrefixes=" + + interestedKeyPrefixes + '}' + "@" + hashCode(); + } + + public abstract void configChanged(ConfigChangeEvent event); +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ObjectUtils.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ObjectUtils.java new file mode 100644 index 0000000000..cea114f48d --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ObjectUtils.java @@ -0,0 +1,40 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.lang.reflect.Type; + +import org.springframework.util.StringUtils; + +final class ObjectUtils { + + private ObjectUtils() { + } + + public static Object convertToObject(String content, Type clazz) { + if (!StringUtils.hasText(content)) { + return null; + } + + return convertFormJsonContent(content, clazz); + } + + private static Object convertFormJsonContent(String content, Type clazz) { + + return JsonUtils.toObj(content, clazz); + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/PropertiesUtils.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/PropertiesUtils.java new file mode 100644 index 0000000000..1803d93e9c --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/PropertiesUtils.java @@ -0,0 +1,74 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.io.StringReader; +import java.util.Map; +import java.util.Properties; + +import com.alibaba.nacos.common.utils.StringUtils; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.constructor.SafeConstructor; + +final class PropertiesUtils { + + private PropertiesUtils() { + } + + public static Properties convertToProperties(String content) throws Exception { + if (StringUtils.isBlank(content)) { + return new Properties(); + } + try { + return convertFormYamlContent(content); + } + catch (Exception e) { + return convertFormPropertiesContent(content); + } + } + + private static Properties convertFormPropertiesContent(String content) throws Exception { + Properties properties = new Properties(); + properties.load(new StringReader(content)); + return properties; + } + + private static Properties convertFormYamlContent(String content) { + + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + Map yamlMap = yaml.load(content); + + Properties properties = new Properties(); + flattenMap("", yamlMap, properties); + + return properties; + } + + private static void flattenMap(String prefix, Map map, Properties properties) { + for (Map.Entry entry : map.entrySet()) { + String key = + prefix.isEmpty() ? String.valueOf(entry.getKey()) : prefix + "." + String.valueOf(entry.getKey()); + if (entry.getValue() instanceof Map) { + flattenMap(key, (Map) entry.getValue(), properties); + } + else { + properties.setProperty(key, entry.getValue().toString()); + } + } + } +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ScaYamlConfigChangeParser.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ScaYamlConfigChangeParser.java new file mode 100644 index 0000000000..210837fcb5 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/ScaYamlConfigChangeParser.java @@ -0,0 +1,118 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import com.alibaba.nacos.api.config.ConfigChangeItem; +import com.alibaba.nacos.api.exception.NacosException; +import com.alibaba.nacos.api.exception.runtime.NacosRuntimeException; +import com.alibaba.nacos.client.config.impl.YmlChangeParser; +import com.alibaba.nacos.common.utils.StringUtils; +import org.yaml.snakeyaml.LoaderOptions; +import org.yaml.snakeyaml.Yaml; +import org.yaml.snakeyaml.composer.ComposerException; +import org.yaml.snakeyaml.constructor.SafeConstructor; +import org.yaml.snakeyaml.error.MarkedYAMLException; + +public class ScaYamlConfigChangeParser extends YmlChangeParser { + private static final String INVALID_CONSTRUCTOR_ERROR_INFO = "could not determine a constructor for the tag"; + + public ScaYamlConfigChangeParser() { + super(); + } + + @Override + public Map doParse(String oldContent, String newContent, String type) { + Map oldMap = Collections.emptyMap(); + Map newMap = Collections.emptyMap(); + try { + Yaml yaml = new Yaml(new SafeConstructor(new LoaderOptions())); + if (StringUtils.isNotBlank(oldContent)) { + oldMap = yaml.load(oldContent); + oldMap = getFlattenedMap(oldMap); + } + if (StringUtils.isNotBlank(newContent)) { + newMap = yaml.load(newContent); + newMap = getFlattenedMap(newMap); + } + } + catch (MarkedYAMLException e) { + handleYamlException(e); + } + + return filterChangeData(oldMap, newMap); + } + + private void handleYamlException(MarkedYAMLException e) { + if (e.getMessage().startsWith(INVALID_CONSTRUCTOR_ERROR_INFO) || e instanceof ComposerException) { + throw new NacosRuntimeException(NacosException.INVALID_PARAM, + "AbstractConfigChangeListener only support basic java data type for yaml. If you want to listen " + + "key changes for custom classes, please use `Listener` to listener whole yaml configuration and parse it by yourself.", + e); + } + throw e; + } + + private Map getFlattenedMap(Map source) { + Map result = new LinkedHashMap<>(128); + buildFlattenedMap(result, source, null); + return result; + } + + private void buildFlattenedMap(Map result, Map source, String path) { + for (Iterator> itr = source.entrySet().iterator(); itr.hasNext(); ) { + Map.Entry e = itr.next(); + String key = String.valueOf(e.getKey()); + if (StringUtils.isNotBlank(path)) { + if (key.startsWith("[")) { + key = path + key; + } + else { + key = path + '.' + key; + } + } + if (e.getValue() instanceof String) { + result.put(key, e.getValue()); + } + else if (e.getValue() instanceof Map) { + @SuppressWarnings("unchecked") Map map = (Map) e.getValue(); + buildFlattenedMap(result, map, key); + } + else if (e.getValue() instanceof Collection) { + @SuppressWarnings("unchecked") Collection collection = (Collection) e.getValue(); + if (collection.isEmpty()) { + result.put(key, ""); + } + else { + int count = 0; + for (Object object : collection) { + buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key); + } + } + } + else { + result.put(key, (e.getValue() != null ? e.getValue() : "")); + } + } + } + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/TargetRefreshable.java b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/TargetRefreshable.java new file mode 100644 index 0000000000..8f851b9f31 --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/java/com/alibaba/cloud/nacos/annotation/TargetRefreshable.java @@ -0,0 +1,27 @@ +/* + * Copyright 2013-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.alibaba.cloud.nacos.annotation; + +import com.alibaba.nacos.api.config.listener.Listener; + +public interface TargetRefreshable extends Listener { + + Object getTarget(); + + void setTarget(Object target); + +} diff --git a/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/resources/META-INF/services/com.alibaba.nacos.api.config.listener.ConfigChangeParser b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/resources/META-INF/services/com.alibaba.nacos.api.config.listener.ConfigChangeParser new file mode 100644 index 0000000000..97f0b886ad --- /dev/null +++ b/spring-cloud-alibaba-starters/spring-cloud-starter-alibaba-nacos-config/src/main/resources/META-INF/services/com.alibaba.nacos.api.config.listener.ConfigChangeParser @@ -0,0 +1 @@ +com.alibaba.cloud.nacos.annotation.ScaYamlConfigChangeParser \ No newline at end of file