Merge pull request #1715 from rushsky518/develop

receive config change event
This commit is contained in:
Peter Zhu 2020-01-10 15:41:24 +08:00 committed by GitHub
commit 1849d96935
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 971 additions and 26 deletions

View File

@ -62,6 +62,8 @@ public class Constants {
public static final String CONFIG_VERSION = "Config-Version"; public static final String CONFIG_VERSION = "Config-Version";
public static final String CONFIG_TYPE = "Config-Type";
public static final String IF_MODIFIED_SINCE = "If-Modified-Since"; public static final String IF_MODIFIED_SINCE = "If-Modified-Since";
public static final String SPACING_INTERVAL = "client-spacing-interval"; public static final String SPACING_INTERVAL = "client-spacing-interval";

View File

@ -0,0 +1,44 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.config;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import java.util.Collection;
import java.util.Map;
/**
* ConfigChangeEvent
*
* @author rushsky518
*/
public class ConfigChangeEvent {
private Map<String, ConfigChangeItem> data;
public ConfigChangeEvent(Map<String, ConfigChangeItem> data) {
this.data = data;
}
public ConfigChangeItem getChangeItem(String key) {
return data.get(key);
}
public Collection<ConfigChangeItem> getChangeItems() {
return data.values();
}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.config;
/**
* ConfigChangeItem
*
* @author rushsky518
*/
public class ConfigChangeItem {
private String key;
private String oldValue;
private String newValue;
private PropertyChangeType type;
public ConfigChangeItem(String key, String oldValue, String newValue) {
this.key = key;
this.oldValue = oldValue;
this.newValue = newValue;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getOldValue() {
return oldValue;
}
public void setOldValue(String oldValue) {
this.oldValue = oldValue;
}
public String getNewValue() {
return newValue;
}
public void setNewValue(String newValue) {
this.newValue = newValue;
}
public PropertyChangeType getType() {
return type;
}
public void setType(PropertyChangeType type) {
this.type = type;
}
@Override
public String toString() {
return "ConfigChangeItem{" +
"key='" + key + '\'' +
", oldValue='" + oldValue + '\'' +
", newValue='" + newValue + '\'' +
", type=" + type +
'}';
}
}

View File

@ -0,0 +1,30 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 = the "License"");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.config;
/**
* Property Change Type
*
* @author rushsky518
*/
public enum PropertyChangeType {
/** add */
ADDED,
/** modified */
MODIFIED,
/** deleted */
DELETED
}

View File

@ -0,0 +1,45 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.api.config.listener;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import java.io.IOException;
import java.util.Map;
/**
* ConfigChangeParser
*
* @author rushsky518
*/
public interface ConfigChangeParser {
/**
* judge type
* @param type
* @return
*/
boolean isResponsibleFor(String type);
/**
* compare old and new data
* @param oldContent
* @param newContent
* @param type
* @return
* @throws IOException
*/
Map<String, ConfigChangeItem> doParse(String oldContent, String newContent, String type) throws IOException;
}

View File

@ -116,6 +116,10 @@
<artifactId>mockito-core</artifactId> <artifactId>mockito-core</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</dependency>
</dependencies> </dependencies>

View File

@ -170,9 +170,8 @@ public class NacosConfigService implements ConfigService {
} }
try { try {
content = worker.getServerConfig(dataId, group, tenant, timeoutMs); String[] ct = worker.getServerConfig(dataId, group, tenant, timeoutMs);
cr.setContent(ct[0]);
cr.setContent(content);
configFilterChainManager.doFilter(null, cr); configFilterChainManager.doFilter(null, cr);
content = cr.getContent(); content = cr.getContent();

View File

@ -0,0 +1,74 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.api.config.PropertyChangeType;
import com.alibaba.nacos.api.config.listener.ConfigChangeParser;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* AbstractConfigChangeParser
*
* @author rushsky518
*/
public abstract class AbstractConfigChangeParser implements ConfigChangeParser {
private String configType;
public AbstractConfigChangeParser(String configType) {
this.configType = configType;
}
@Override
public boolean isResponsibleFor(String type) {
return this.configType.equalsIgnoreCase(type);
}
protected Map<String, ConfigChangeItem> filterChangeData(Map oldMap, Map newMap) {
Map<String, ConfigChangeItem> result = new HashMap<String, ConfigChangeItem>(16);
for (Iterator<Map.Entry<String, Object>> entryItr = oldMap.entrySet().iterator(); entryItr.hasNext();) {
Map.Entry<String, Object> e = entryItr.next();
ConfigChangeItem cci = null;
if (newMap.containsKey(e.getKey())) {
if (e.getValue().equals(newMap.get(e.getKey()))) {
continue;
}
cci = new ConfigChangeItem(e.getKey(), e.getValue().toString(), newMap.get(e.getKey()).toString());
cci.setType(PropertyChangeType.MODIFIED);
} else {
cci = new ConfigChangeItem(e.getKey(), e.getValue().toString(), null);
cci.setType(PropertyChangeType.DELETED);
}
result.put(e.getKey(), cci);
}
for (Iterator<Map.Entry<String, Object>> entryItr = newMap.entrySet().iterator(); entryItr.hasNext();) {
Map.Entry<String, Object> e = entryItr.next();
if (!oldMap.containsKey(e.getKey())) {
ConfigChangeItem cci = new ConfigChangeItem(e.getKey(), null, e.getValue().toString());
cci.setType(PropertyChangeType.ADDED);
result.put(e.getKey(), cci);
}
}
return result;
}
}

View File

@ -16,11 +16,13 @@
package com.alibaba.nacos.client.config.impl; package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.ConfigChangeEvent;
import com.alibaba.nacos.api.config.listener.AbstractSharedListener; import com.alibaba.nacos.api.config.listener.AbstractSharedListener;
import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.config.filter.impl.ConfigFilterChainManager; import com.alibaba.nacos.client.config.filter.impl.ConfigFilterChainManager;
import com.alibaba.nacos.client.config.filter.impl.ConfigResponse; import com.alibaba.nacos.client.config.filter.impl.ConfigResponse;
import com.alibaba.nacos.client.config.listener.impl.AbstractConfigChangeListener;
import com.alibaba.nacos.client.config.utils.MD5; import com.alibaba.nacos.client.config.utils.MD5;
import com.alibaba.nacos.client.utils.LogUtils; import com.alibaba.nacos.client.utils.LogUtils;
import com.alibaba.nacos.client.utils.TenantUtil; import com.alibaba.nacos.client.utils.TenantUtil;
@ -28,6 +30,7 @@ import org.slf4j.Logger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
/** /**
@ -59,9 +62,17 @@ public class CacheData {
return content; return content;
} }
public void setContent(String newContent) { public void setContent(String content) {
this.content = newContent; this.content = content;
this.md5 = getMd5String(content); this.md5 = getMd5String(this.content);
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
} }
/** /**
@ -74,7 +85,9 @@ public class CacheData {
if (null == listener) { if (null == listener) {
throw new IllegalArgumentException("listener is null"); throw new IllegalArgumentException("listener is null");
} }
ManagerListenerWrap wrap = new ManagerListenerWrap(listener, md5); ManagerListenerWrap wrap = (listener instanceof AbstractConfigChangeListener) ?
new ManagerListenerWrap(listener, md5, content) : new ManagerListenerWrap(listener, md5);
if (listeners.addIfAbsent(wrap)) { if (listeners.addIfAbsent(wrap)) {
LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group, LOGGER.info("[{}] [add-listener] ok, tenant={}, dataId={}, group={}, cnt={}", name, tenant, dataId, group,
listeners.size()); listeners.size());
@ -158,12 +171,12 @@ public class CacheData {
void checkListenerMd5() { void checkListenerMd5() {
for (ManagerListenerWrap wrap : listeners) { for (ManagerListenerWrap wrap : listeners) {
if (!md5.equals(wrap.lastCallMd5)) { if (!md5.equals(wrap.lastCallMd5)) {
safeNotifyListener(dataId, group, content, md5, wrap); safeNotifyListener(dataId, group, content, type, md5, wrap);
} }
} }
} }
private void safeNotifyListener(final String dataId, final String group, final String content, private void safeNotifyListener(final String dataId, final String group, final String content, final String type,
final String md5, final ManagerListenerWrap listenerWrap) { final String md5, final ManagerListenerWrap listenerWrap) {
final Listener listener = listenerWrap.listener; final Listener listener = listenerWrap.listener;
@ -188,6 +201,15 @@ public class CacheData {
configFilterChainManager.doFilter(null, cr); configFilterChainManager.doFilter(null, cr);
String contentTmp = cr.getContent(); String contentTmp = cr.getContent();
listener.receiveConfigInfo(contentTmp); listener.receiveConfigInfo(contentTmp);
// compare lastContent and content
if (listener instanceof AbstractConfigChangeListener) {
Map data = ConfigChangeHandler.getInstance().parseChangeData(listenerWrap.lastContent, content, type);
ConfigChangeEvent event = new ConfigChangeEvent(data);
((AbstractConfigChangeListener)listener).receiveConfigChange(event);
listenerWrap.lastContent = content;
}
listenerWrap.lastCallMd5 = md5; listenerWrap.lastCallMd5 = md5;
LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5, LOGGER.info("[{}] [notify-ok] dataId={}, group={}, md5={}, listener={} ", name, dataId, group, md5,
listener); listener);
@ -282,11 +304,13 @@ public class CacheData {
private volatile String content; private volatile String content;
private int taskId; private int taskId;
private volatile boolean isInitializing = true; private volatile boolean isInitializing = true;
private String type;
} }
class ManagerListenerWrap { class ManagerListenerWrap {
final Listener listener; final Listener listener;
String lastCallMd5 = CacheData.getMd5String(null); String lastCallMd5 = CacheData.getMd5String(null);
String lastContent = null;
ManagerListenerWrap(Listener listener) { ManagerListenerWrap(Listener listener) {
this.listener = listener; this.listener = listener;
@ -297,6 +321,12 @@ class ManagerListenerWrap {
this.lastCallMd5 = md5; this.lastCallMd5 = md5;
} }
ManagerListenerWrap(Listener listener, String md5, String lastContent) {
this.listener = listener;
this.lastCallMd5 = md5;
this.lastContent = lastContent;
}
@Override @Override
public boolean equals(Object obj) { public boolean equals(Object obj) {
if (null == obj || obj.getClass() != getClass()) { if (null == obj || obj.getClass() != getClass()) {

View File

@ -17,6 +17,7 @@ package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.PropertyKeyConst; import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.common.Constants; import com.alibaba.nacos.api.common.Constants;
import com.alibaba.nacos.api.config.ConfigType;
import com.alibaba.nacos.api.config.listener.Listener; import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException; import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.config.common.GroupKey; import com.alibaba.nacos.client.config.common.GroupKey;
@ -46,6 +47,7 @@ import java.util.concurrent.atomic.AtomicReference;
import static com.alibaba.nacos.api.common.Constants.LINE_SEPARATOR; import static com.alibaba.nacos.api.common.Constants.LINE_SEPARATOR;
import static com.alibaba.nacos.api.common.Constants.WORD_SEPARATOR; import static com.alibaba.nacos.api.common.Constants.WORD_SEPARATOR;
import static com.alibaba.nacos.api.common.Constants.CONFIG_TYPE;
/** /**
* Longpolling * Longpolling
@ -183,8 +185,8 @@ public class ClientWorker {
cache = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant); cache = new CacheData(configFilterChainManager, agent.getName(), dataId, group, tenant);
// fix issue # 1317 // fix issue # 1317
if (enableRemoteSyncConfig) { if (enableRemoteSyncConfig) {
String content = getServerConfig(dataId, group, tenant, 3000L); String[] ct = getServerConfig(dataId, group, tenant, 3000L);
cache.setContent(content); cache.setContent(ct[0]);
} }
} }
@ -210,8 +212,9 @@ public class ClientWorker {
return cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant)); return cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
} }
public String getServerConfig(String dataId, String group, String tenant, long readTimeout) public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout)
throws NacosException { throws NacosException {
String[] ct = new String[2];
if (StringUtils.isBlank(group)) { if (StringUtils.isBlank(group)) {
group = Constants.DEFAULT_GROUP; group = Constants.DEFAULT_GROUP;
} }
@ -236,10 +239,16 @@ public class ClientWorker {
switch (result.code) { switch (result.code) {
case HttpURLConnection.HTTP_OK: case HttpURLConnection.HTTP_OK:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content); LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, result.content);
return result.content; ct[0] = result.content;
if (result.headers.containsKey(CONFIG_TYPE)) {
ct[1] = result.headers.get(CONFIG_TYPE).get(0);
} else {
ct[1] = ConfigType.TEXT.getType();
}
return ct;
case HttpURLConnection.HTTP_NOT_FOUND: case HttpURLConnection.HTTP_NOT_FOUND:
LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null); LocalConfigInfoProcessor.saveSnapshot(agent.getName(), dataId, group, tenant, null);
return null; return ct;
case HttpURLConnection.HTTP_CONFLICT: { case HttpURLConnection.HTTP_CONFLICT: {
LOGGER.error( LOGGER.error(
"[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, " "[{}] [sub-server-error] get server config being modified concurrently, dataId={}, group={}, "
@ -517,12 +526,15 @@ public class ClientWorker {
tenant = key[2]; tenant = key[2];
} }
try { try {
String content = getServerConfig(dataId, group, tenant, 3000L); String[] ct = getServerConfig(dataId, group, tenant, 3000L);
CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant)); CacheData cache = cacheMap.get().get(GroupKey.getKeyTenant(dataId, group, tenant));
cache.setContent(content); cache.setContent(ct[0]);
LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}", if (null != ct[1]) {
cache.setType(ct[1]);
}
LOGGER.info("[{}] [data-received] dataId={}, group={}, tenant={}, md5={}, content={}, type={}",
agent.getName(), dataId, group, tenant, cache.getMd5(), agent.getName(), dataId, group, tenant, cache.getMd5(),
ContentUtils.truncateContent(content)); ContentUtils.truncateContent(ct[0]), ct[1]);
} catch (NacosException ioe) { } catch (NacosException ioe) {
String message = String.format( String message = String.format(
"[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s", "[%s] [get-update] get changed config exception. dataId=%s, group=%s, tenant=%s",

View File

@ -0,0 +1,67 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.config.listener.ConfigChangeParser;
import java.io.IOException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.Map;
import java.util.Iterator;
/**
* ConfigChangeHandler
*
* @author rushsky518
*/
public class ConfigChangeHandler {
private static class ConfigChangeHandlerHolder {
private final static ConfigChangeHandler INSTANCE = new ConfigChangeHandler();
}
private ConfigChangeHandler() {
this.parserList = new LinkedList<ConfigChangeParser>();
ServiceLoader<ConfigChangeParser> loader = ServiceLoader.load(ConfigChangeParser.class);
Iterator<ConfigChangeParser> itr = loader.iterator();
while (itr.hasNext()) {
this.parserList.add(itr.next());
}
this.parserList.add(new PropertiesChangeParser());
this.parserList.add(new YmlChangeParser());
}
public static ConfigChangeHandler getInstance() {
return ConfigChangeHandlerHolder.INSTANCE;
}
public Map parseChangeData(String oldContent, String newContent, String type) throws IOException {
for (ConfigChangeParser changeParser: this.parserList) {
if (changeParser.isResponsibleFor(type)) {
return changeParser.doParse(oldContent, newContent, type);
}
}
return Collections.emptyMap();
}
private List<ConfigChangeParser> parserList;
}

View File

@ -0,0 +1,50 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.client.utils.StringUtils;
import java.io.IOException;
import java.io.StringReader;
import java.util.Map;
import java.util.Properties;
/**
* PropertiesChangeParser
*
* @author rushsky518
*/
public class PropertiesChangeParser extends AbstractConfigChangeParser {
public PropertiesChangeParser() {
super("properties");
}
@Override
public Map<String, ConfigChangeItem> doParse(String oldContent, String newContent, String type) throws IOException {
Properties oldProps = new Properties();
Properties newProps = new Properties();
if (StringUtils.isNotBlank(oldContent)) {
oldProps.load(new StringReader(oldContent));
}
if (StringUtils.isNotBlank(newContent)) {
newProps.load(new StringReader(newContent));
}
return filterChangeData(oldProps, newProps);
}
}

View File

@ -0,0 +1,90 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.client.utils.StringUtils;
import org.yaml.snakeyaml.Yaml;
import java.util.*;
/**
* YmlChangeParser
*
* @author rushsky518
*/
public class YmlChangeParser extends AbstractConfigChangeParser {
public YmlChangeParser() {
super("yaml");
}
@Override
public Map<String, ConfigChangeItem> doParse(String oldContent, String newContent, String type) {
Map<String, Object> oldMap = Collections.emptyMap();
Map<String, Object> newMap = Collections.emptyMap();
if (StringUtils.isNotBlank(oldContent)) {
oldMap = (new Yaml()).load(oldContent);
oldMap = getFlattenedMap(oldMap);
}
if (StringUtils.isNotBlank(newContent)) {
newMap = (new Yaml()).load(newContent);
newMap = getFlattenedMap(newMap);
}
return filterChangeData(oldMap, newMap);
}
private final Map<String, Object> getFlattenedMap(Map<String, Object> source) {
Map<String, Object> result = new LinkedHashMap<String, Object>(128);
buildFlattenedMap(result, source, null);
return result;
}
private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
for (Iterator<Map.Entry<String, Object>> itr = source.entrySet().iterator(); itr.hasNext(); ) {
Map.Entry<String, Object> e = itr.next();
String key = e.getKey();
if (StringUtils.isNotBlank(path)) {
if (e.getKey().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<String, Object> map = (Map<String, Object>) e.getValue();
buildFlattenedMap(result, map, key);
} else if (e.getValue() instanceof Collection) {
@SuppressWarnings("unchecked")
Collection<Object> collection = (Collection<Object>) 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() : ""));
}
}
}
}

View File

@ -0,0 +1,36 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.listener.impl;
import com.alibaba.nacos.api.config.listener.AbstractListener;
import com.alibaba.nacos.api.config.ConfigChangeEvent;
/**
* AbstractConfigChangeListener
*
* @author rushsky518
*/
public abstract class AbstractConfigChangeListener extends AbstractListener {
/**
* handle config change
* @param event
*/
public abstract void receiveConfigChange(final ConfigChangeEvent event);
@Override
public void receiveConfigInfo(final String configInfo) {}
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.listener.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.client.config.impl.ConfigChangeHandler;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
public class ConfigChangeHandlerTest {
@Test
public void testParseProperties() throws IOException {
Map properties = ConfigChangeHandler.getInstance().parseChangeData("", "app.name = nacos", "properties");
Assert.assertEquals("nacos", ((ConfigChangeItem)properties.get("app.name")).getNewValue());
}
@Test
public void testParseYaml() throws IOException {
Map properties = ConfigChangeHandler.getInstance().parseChangeData("", "app:\n name: nacos", "yaml");
Assert.assertEquals("nacos", ((ConfigChangeItem)properties.get("app.name")).getNewValue());
}
}

View File

@ -0,0 +1,58 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.listener.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.client.config.impl.PropertiesChangeParser;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
public class PropertiesChangeParserTest {
private PropertiesChangeParser parser = new PropertiesChangeParser();
private final String type = "properties";
@Test
public void testType() {
Assert.assertEquals(true, parser.isResponsibleFor(type));
}
@Test
public void testAddKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("", "app.name = nacos", type);
Assert.assertEquals(null, map.get("app.name").getOldValue());
Assert.assertEquals("nacos", map.get("app.name").getNewValue());
}
@Test
public void testRemoveKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("app.name = nacos", "", type);
Assert.assertEquals("nacos", map.get("app.name").getOldValue());
Assert.assertEquals(null, map.get("app.name").getNewValue());
}
@Test
public void testModifyKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("app.name = rocketMQ", "app.name = nacos", type);
Assert.assertEquals("rocketMQ", map.get("app.name").getOldValue());
Assert.assertEquals("nacos", map.get("app.name").getNewValue());
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.client.config.listener.impl;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.client.config.impl.YmlChangeParser;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.util.Map;
public class YmlChangeParserTest {
private YmlChangeParser parser = new YmlChangeParser();
private final String type = "yaml";
@Test
public void testType() {
Assert.assertEquals(true, parser.isResponsibleFor(type));
}
@Test
public void testAddKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("", "app:\n name: nacos", type);
Assert.assertEquals(null, map.get("app.name").getOldValue());
Assert.assertEquals("nacos", map.get("app.name").getNewValue());
}
@Test
public void testRemoveKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("app:\n name: nacos", "", type);
Assert.assertEquals("nacos", map.get("app.name").getOldValue());
Assert.assertEquals(null, map.get("app.name").getNewValue());
}
@Test
public void testModifyKey() throws IOException {
Map<String, ConfigChangeItem> map = parser.doParse("app:\n name: rocketMQ", "app:\n name: nacos", type);
Assert.assertEquals("rocketMQ", map.get("app.name").getOldValue());
Assert.assertEquals("nacos", map.get("app.name").getNewValue());
}
}

View File

@ -130,6 +130,8 @@ public class ConfigServletInner {
isBeta = true; isBeta = true;
} }
} }
String configType = cacheItem.getType();
response.setHeader("Config-Type", (null != configType) ? configType : "text");
} }
File file = null; File file = null;
ConfigInfoBase configInfoBase = null; ConfigInfoBase configInfoBase = null;

View File

@ -109,6 +109,14 @@ public class CacheItem {
this.tagLastModifiedTs = tagLastModifiedTs; this.tagLastModifiedTs = tagLastModifiedTs;
} }
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
final String groupKey; final String groupKey;
public volatile String md5 = Constants.NULL; public volatile String md5 = Constants.NULL;
public volatile long lastModifiedTs; public volatile long lastModifiedTs;
@ -123,5 +131,6 @@ public class CacheItem {
public volatile Map<String, String> tagMd5; public volatile Map<String, String> tagMd5;
public volatile Map<String, Long> tagLastModifiedTs; public volatile Map<String, Long> tagLastModifiedTs;
public SimpleReadWriteLock rwLock = new SimpleReadWriteLock(); public SimpleReadWriteLock rwLock = new SimpleReadWriteLock();
public String type;
} }

View File

@ -28,6 +28,8 @@ public class ConfigInfo extends ConfigInfoBase {
private String appName; private String appName;
private String type;
public ConfigInfo() { public ConfigInfo() {
} }
@ -63,6 +65,14 @@ public class ConfigInfo extends ConfigInfoBase {
this.appName = appName; this.appName = appName;
} }
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
@Override @Override
public int hashCode() { public int hashCode() {
return super.hashCode(); return super.hashCode();

View File

@ -57,9 +57,10 @@ public class ConfigService {
/** /**
* 保存配置文件并缓存md5. * 保存配置文件并缓存md5.
*/ */
static public boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs) { static public boolean dump(String dataId, String group, String tenant, String content, long lastModifiedTs, String type) {
String groupKey = GroupKey2.getKey(dataId, group, tenant); String groupKey = GroupKey2.getKey(dataId, group, tenant);
makeSure(groupKey); CacheItem ci = makeSure(groupKey);
ci.setType(type);
final int lockResult = tryWriteLock(groupKey); final int lockResult = tryWriteLock(groupKey);
assert (lockResult != 0); assert (lockResult != 0);

View File

@ -113,6 +113,7 @@ public class PersistService {
info.setGroup(rs.getString("group_id")); info.setGroup(rs.getString("group_id"));
info.setTenant(rs.getString("tenant_id")); info.setTenant(rs.getString("tenant_id"));
info.setAppName(rs.getString("app_name")); info.setAppName(rs.getString("app_name"));
info.setType(rs.getString("type"));
try { try {
info.setContent(rs.getString("content")); info.setContent(rs.getString("content"));
@ -235,6 +236,11 @@ public class PersistService {
} catch (SQLException e) { } catch (SQLException e) {
// ignore // ignore
} }
try {
info.setType(rs.getString("type"));
} catch (SQLException e) {
// ignore
}
return info; return info;
} }
} }
@ -1325,7 +1331,7 @@ public class PersistService {
final String appName = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("appName"); final String appName = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("appName");
final String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags"); final String configTags = configAdvanceInfo == null ? null : (String) configAdvanceInfo.get("config_tags");
String sqlCount = "select count(*) from config_info"; String sqlCount = "select count(*) from config_info";
String sql = "select ID,data_id,group_id,tenant_id,app_name,content from config_info"; String sql = "select ID,data_id,group_id,tenant_id,app_name,content,type from config_info";
StringBuilder where = new StringBuilder(" where "); StringBuilder where = new StringBuilder(" where ");
List<String> paramList = new ArrayList<String>(); List<String> paramList = new ArrayList<String>();
paramList.add(tenantTmp); paramList.add(tenantTmp);
@ -1944,7 +1950,7 @@ public class PersistService {
public Page<ConfigInfoWrapper> findAllConfigInfoFragment(final long lastMaxId, final int pageSize) { public Page<ConfigInfoWrapper> findAllConfigInfoFragment(final long lastMaxId, final int pageSize) {
String select String select
= "SELECT id,data_id,group_id,tenant_id,app_name,content,md5,gmt_modified from config_info where id > ? " = "SELECT id,data_id,group_id,tenant_id,app_name,content,md5,gmt_modified,type from config_info where id > ? "
+ "order by id asc limit ?,?"; + "order by id asc limit ?,?";
PaginationHelper<ConfigInfoWrapper> helper = new PaginationHelper<ConfigInfoWrapper>(); PaginationHelper<ConfigInfoWrapper> helper = new PaginationHelper<ConfigInfoWrapper>();
try { try {
@ -2927,8 +2933,8 @@ public class PersistService {
final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant; final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant;
try { try {
return this.jt.queryForObject( return this.jt.queryForObject(
"SELECT ID,data_id,group_id,tenant_id,app_name,content,md5 FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?",
new Object[]{dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER); new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER);
} catch (EmptyResultDataAccessException e) { // 表明数据不存在, 返回null } catch (EmptyResultDataAccessException e) { // 表明数据不存在, 返回null
return null; return null;
} catch (CannotGetJdbcConnectionException e) { } catch (CannotGetJdbcConnectionException e) {

View File

@ -187,7 +187,7 @@ class DumpProcessor implements TaskProcessor {
boolean result; boolean result;
if (null != cf) { if (null != cf) {
result = ConfigService.dump(dataId, group, tenant, cf.getContent(), lastModified); result = ConfigService.dump(dataId, group, tenant, cf.getContent(), lastModified, cf.getType());
if (result) { if (result) {
ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp, ConfigTraceService.logDumpEvent(dataId, group, tenant, null, lastModified, handleIp,
@ -261,7 +261,7 @@ class DumpAllProcessor implements TaskProcessor {
} }
boolean result = ConfigService.dump(cf.getDataId(), cf.getGroup(), cf.getTenant(), cf.getContent(), boolean result = ConfigService.dump(cf.getDataId(), cf.getGroup(), cf.getTenant(), cf.getContent(),
cf.getLastModified()); cf.getLastModified(), cf.getType());
final String content = cf.getContent(); final String content = cf.getContent();
final String md5 = MD5.getInstance().getMD5String(content); final String md5 = MD5.getInstance().getMD5String(content);

View File

@ -0,0 +1,151 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.test.config;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.config.ConfigChangeEvent;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.PropertyChangeType;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.client.config.listener.impl.AbstractConfigChangeListener;
import com.alibaba.nacos.config.server.Config;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.Properties;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Config.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ConfigLongPollReturnChanges_ITCase {
@LocalServerPort
private int port;
private ConfigService configService;
@Before
public void init() throws NacosException {
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "127.0.0.1:" + port);
properties.put(PropertyKeyConst.CONFIG_LONG_POLL_TIMEOUT, "20000");
properties.put(PropertyKeyConst.CONFIG_RETRY_TIME, "3000");
properties.put(PropertyKeyConst.MAX_RETRY, "5");
configService = NacosFactory.createConfigService(properties);
}
@Test
public void testAdd() throws InterruptedException, NacosException {
CountDownLatch latch = new CountDownLatch(1);
final String dataId = "test" + System.currentTimeMillis();
final String group = "DEFAULT_GROUP";
final String content = "config data";
configService.addListener(dataId, group, new AbstractConfigChangeListener() {
@Override
public void receiveConfigChange(ConfigChangeEvent event) {
ConfigChangeItem cci = event.getChangeItem("content");
Assert.assertEquals(null, cci.getOldValue());
Assert.assertEquals(content, cci.getNewValue());
Assert.assertEquals(PropertyChangeType.ADDED, cci.getType());
System.out.println(cci);
latch.countDown();
}
});
configService.publishConfig(dataId, group, content);
latch.await();
}
@Test
public void testModify() throws InterruptedException, NacosException {
CountDownLatch latch = new CountDownLatch(1);
final String dataId = "test" + System.currentTimeMillis();
final String group = "DEFAULT_GROUP";
final String oldData = "old data";
final String newData = "new data";
configService.publishConfig(dataId, group, oldData);
// query config immediately may return null
String config = null;
do {
TimeUnit.SECONDS.sleep(1);
config = configService.getConfig(dataId, group, 50);
} while(null == config);
configService.addListener(dataId, group, new AbstractConfigChangeListener() {
@Override
public void receiveConfigChange(ConfigChangeEvent event) {
ConfigChangeItem cci = event.getChangeItem("content");
Assert.assertEquals(oldData, cci.getOldValue());
Assert.assertEquals(newData, cci.getNewValue());
Assert.assertEquals(PropertyChangeType.MODIFIED, cci.getType());
System.out.println(cci);
latch.countDown();
}
});
configService.publishConfig(dataId, group, newData);
latch.await();
}
@Test
public void testDelete() throws InterruptedException, NacosException {
CountDownLatch latch = new CountDownLatch(1);
final String dataId = "test" + System.currentTimeMillis();
final String group = "DEFAULT_GROUP";
final String oldData = "old data";
configService.publishConfig(dataId, group, oldData);
// query config immediately may return null
String config = null;
do {
TimeUnit.SECONDS.sleep(1);
config = configService.getConfig(dataId, group, 50);
} while(null == config);
configService.addListener(dataId, group, new AbstractConfigChangeListener() {
@Override
public void receiveConfigChange(ConfigChangeEvent event) {
ConfigChangeItem cci = event.getChangeItem("content");
Assert.assertEquals(oldData, cci.getOldValue());
Assert.assertEquals(null, cci.getNewValue());
Assert.assertEquals(PropertyChangeType.DELETED, cci.getType());
System.out.println(cci);
latch.countDown();
}
});
configService.removeConfig(dataId, group);
latch.await();
}
}

View File

@ -0,0 +1,51 @@
/*
* Copyright 1999-2018 Alibaba Group Holding Ltd.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.alibaba.nacos.test.config;
import com.alibaba.nacos.api.config.ConfigChangeItem;
import com.alibaba.nacos.api.config.PropertyChangeType;
import com.alibaba.nacos.api.config.listener.ConfigChangeParser;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class TextChangeParser implements ConfigChangeParser {
@Override
public boolean isResponsibleFor(String type) {
return (null == type || "text".equalsIgnoreCase(type));
}
@Override
public Map<String, ConfigChangeItem> doParse(String oldContent, String newContent, String type) throws IOException {
Map<String, ConfigChangeItem> map = new HashMap<>(4);
final String key = "content";
ConfigChangeItem cci = new ConfigChangeItem(key, oldContent, newContent);
if (null == oldContent && null != newContent) {
cci.setType(PropertyChangeType.ADDED);
} else if (null != oldContent && null != newContent && !oldContent.equals(newContent)) {
cci.setType(PropertyChangeType.MODIFIED);
} else if (null != oldContent && null == newContent) {
cci.setType(PropertyChangeType.DELETED);
}
map.put(key, cci);
return map;
}
}

View File

@ -0,0 +1 @@
com.alibaba.nacos.test.config.TextChangeParser