瀏覽代碼

Add config file controller to support get config as plain properties file

Jason Song 8 年之前
父節點
當前提交
f435f27d34
共有 24 個文件被更改,包括 691 次插入111 次删除
  1. 1 1
      apollo-adminservice/pom.xml
  2. 1 1
      apollo-assembly/pom.xml
  3. 1 1
      apollo-biz/pom.xml
  4. 1 1
      apollo-buildtools/pom.xml
  5. 1 1
      apollo-client/pom.xml
  6. 4 27
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java
  7. 1 1
      apollo-common/pom.xml
  8. 1 1
      apollo-configservice/pom.xml
  9. 5 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java
  10. 187 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileController.java
  11. 9 66
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationController.java
  12. 88 0
      apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/WatchKeysUtil.java
  13. 4 1
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/AllTests.java
  14. 164 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileControllerTest.java
  15. 5 1
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerTest.java
  16. 39 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/AbstractBaseIntegrationTest.java
  17. 128 0
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/ConfigFileControllerIntegrationTest.java
  18. 2 4
      apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/NotificationControllerIntegrationTest.java
  19. 1 1
      apollo-core/pom.xml
  20. 42 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/PropertiesUtil.java
  21. 1 1
      apollo-demo/pom.xml
  22. 3 1
      apollo-demo/src/main/java/ApolloConfigFileDemo.java
  23. 1 1
      apollo-portal/pom.xml
  24. 1 1
      pom.xml

+ 1 - 1
apollo-adminservice/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 1 - 1
apollo-assembly/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 1 - 1
apollo-biz/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<artifactId>apollo</artifactId>
 		<groupId>com.ctrip.framework.apollo</groupId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>
 	<artifactId>apollo-biz</artifactId>

+ 1 - 1
apollo-buildtools/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 1 - 1
apollo-client/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 4 - 27
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java

@@ -1,6 +1,7 @@
 package com.ctrip.framework.apollo.internals;
 
 import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.core.utils.PropertiesUtil;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
 import com.ctrip.framework.apollo.util.ExceptionUtil;
 import com.dianping.cat.Cat;
@@ -8,8 +9,6 @@ import com.dianping.cat.Cat;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.IOException;
-import java.io.StringWriter;
 import java.util.Properties;
 import java.util.concurrent.atomic.AtomicReference;
 
@@ -38,13 +37,10 @@ public class PropertiesConfigFile extends AbstractConfigFile {
     if (m_configProperties.get() == null) {
       return null;
     }
-    StringWriter writer = new StringWriter();
+
     try {
-      m_configProperties.get().store(writer, null);
-      StringBuffer stringBuffer = writer.getBuffer();
-      filterPropertiesComment(stringBuffer);
-      return stringBuffer.toString();
-    } catch (IOException ex) {
+      return PropertiesUtil.toString(m_configProperties.get());
+    } catch (Throwable ex) {
       ApolloConfigException exception =
           new ApolloConfigException(String
               .format("Parse properties file content failed for namespace: %s, cause: %s",
@@ -54,25 +50,6 @@ public class PropertiesConfigFile extends AbstractConfigFile {
     }
   }
 
-  /**
-   * filter out the first comment line
-   * @param stringBuffer the string buffer
-   * @return true if filtered successfully, false otherwise
-   */
-  boolean filterPropertiesComment(StringBuffer stringBuffer) {
-    //check whether has comment in the first line
-    if (stringBuffer.charAt(0) != '#') {
-      return false;
-    }
-    int commentLineIndex = stringBuffer.indexOf("\n");
-    if (commentLineIndex == -1) {
-      return false;
-    }
-    stringBuffer.delete(0, commentLineIndex + 1);
-    return true;
-  }
-
-
   @Override
   public boolean hasContent() {
     return m_configProperties.get() != null && !m_configProperties.get().isEmpty();

+ 1 - 1
apollo-common/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 1 - 1
apollo-configservice/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 5 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/ConfigServiceAutoConfiguration.java

@@ -1,6 +1,7 @@
 package com.ctrip.framework.apollo.configservice;
 
 import com.ctrip.framework.apollo.biz.message.ReleaseMessageScanner;
+import com.ctrip.framework.apollo.configservice.controller.ConfigFileController;
 import com.ctrip.framework.apollo.configservice.controller.NotificationController;
 
 import org.springframework.beans.factory.annotation.Autowired;
@@ -14,10 +15,14 @@ import org.springframework.context.annotation.Configuration;
 public class ConfigServiceAutoConfiguration {
   @Autowired
   private NotificationController notificationController;
+  @Autowired
+  private ConfigFileController configFileController;
 
   @Bean
   public ReleaseMessageScanner releaseMessageScanner() {
     ReleaseMessageScanner releaseMessageScanner = new ReleaseMessageScanner();
+    //handle server cache first
+    releaseMessageScanner.addMessageListener(configFileController);
     releaseMessageScanner.addMessageListener(notificationController);
     return releaseMessageScanner;
   }

+ 187 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileController.java

@@ -0,0 +1,187 @@
+package com.ctrip.framework.apollo.configservice.controller;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.common.cache.Weigher;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Multimaps;
+
+import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
+import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener;
+import com.ctrip.framework.apollo.biz.message.Topics;
+import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
+import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.core.dto.ApolloConfig;
+import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
+import com.ctrip.framework.apollo.core.utils.PropertiesUtil;
+import com.dianping.cat.Cat;
+
+import org.hibernate.cache.spi.CacheKey;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.context.request.async.DeferredResult;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+@RestController
+@RequestMapping("/configfiles")
+public class ConfigFileController implements ReleaseMessageListener{
+  private static final Logger logger = LoggerFactory.getLogger(ConfigFileController.class);
+  private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
+  private static final long MAX_CACHE_SIZE = 50 * 1024 * 1024; // 50MB
+  private static final long EXPIRE_AFTER_WRITE = 10;
+  private final HttpHeaders responseHeaders;
+  private final ResponseEntity<String> NOT_FOUND_RESPONSE;
+  private Cache<String, String> localCache;
+  private final Multimap<String, String>
+      watchedKeys2CacheKey = Multimaps.synchronizedSetMultimap(HashMultimap.create());
+  private final Multimap<String, String>
+      cacheKey2WatchedKeys = Multimaps.synchronizedSetMultimap(HashMultimap.create());
+
+  @Autowired
+  private ConfigController configController;
+
+  @Autowired
+  private NamespaceUtil namespaceUtil;
+
+  @Autowired
+  private WatchKeysUtil watchKeysUtil;
+
+  public ConfigFileController() {
+    localCache = CacheBuilder.newBuilder()
+        .expireAfterWrite(EXPIRE_AFTER_WRITE, TimeUnit.MINUTES)
+        .weigher(new Weigher<String, String>() {
+          @Override
+          public int weigh(String key, String value) {
+            return value == null ? 0 : value.length();
+          }
+        })
+        .maximumWeight(MAX_CACHE_SIZE)
+        .removalListener(new RemovalListener<String, String>() {
+          @Override
+          public void onRemoval(RemovalNotification<String, String> notification) {
+            String cacheKey = notification.getKey();
+            logger.debug("removing cache key: {}", cacheKey);
+            if (!cacheKey2WatchedKeys.containsKey(cacheKey)) {
+              return;
+            }
+            //create a new list to avoid ConcurrentModificationException
+            List<String> watchedKeys = new ArrayList<>(cacheKey2WatchedKeys.get(cacheKey));
+            for (String watchedKey : watchedKeys) {
+              watchedKeys2CacheKey.remove(watchedKey, cacheKey);
+            }
+            cacheKey2WatchedKeys.removeAll(cacheKey);
+            logger.debug("removed cache key: {}", cacheKey);
+          }
+        })
+        .build();
+    responseHeaders = new HttpHeaders();
+    responseHeaders.add("Content-Type", "text/plain;charset=UTF-8");
+    NOT_FOUND_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_FOUND);
+  }
+
+  @RequestMapping(value = "/{appId}/{clusterName}/{namespace:.+}", method = RequestMethod.GET)
+  public ResponseEntity<String> queryConfigAsFile(@PathVariable String appId,
+                                                  @PathVariable String clusterName,
+                                                  @PathVariable String namespace,
+                                                  @RequestParam(value = "dataCenter", required = false) String dataCenter,
+                                                  @RequestParam(value = "ip", required = false) String clientIp,
+                                                  HttpServletResponse response) throws IOException {
+    //strip out .properties suffix
+    namespace = namespaceUtil.filterNamespaceName(namespace);
+
+    //TODO add clientIp as key parts?
+    String cacheKey = assembleCacheKey(appId, clusterName, namespace, dataCenter);
+
+    String result = localCache.getIfPresent(cacheKey);
+
+    if (Strings.isNullOrEmpty(result)) {
+      ApolloConfig apolloConfig =
+          configController
+              .queryConfig(appId, clusterName, namespace, dataCenter, "-1", clientIp,
+                  response);
+
+      if (apolloConfig == null || apolloConfig.getConfigurations() == null) {
+        return NOT_FOUND_RESPONSE;
+      }
+      Properties properties = new Properties();
+      properties.putAll(apolloConfig.getConfigurations());
+      result = PropertiesUtil.toString(properties);
+
+      localCache.put(cacheKey, result);
+      logger.debug("adding cache for key: {}", cacheKey);
+
+      Set<String> watchedKeys =
+          watchKeysUtil.assembleAllWatchKeys(appId, clusterName, namespace, dataCenter);
+
+      for (String watchedKey : watchedKeys) {
+        watchedKeys2CacheKey.put(watchedKey, cacheKey);
+      }
+
+      cacheKey2WatchedKeys.putAll(cacheKey, watchedKeys);
+      logger.debug("added cache for key: {}", cacheKey);
+    }
+
+    return new ResponseEntity<>(result, responseHeaders,
+        HttpStatus.OK);
+  }
+
+  String assembleCacheKey(String appId, String clusterName, String namespace,
+                                  String dataCenter) {
+    List<String> keyParts = Lists.newArrayList(appId, clusterName, namespace);
+    if (!Strings.isNullOrEmpty(dataCenter)) {
+      keyParts.add(dataCenter);
+    }
+    return STRING_JOINER.join(keyParts);
+  }
+
+  @Override
+  public void handleMessage(ReleaseMessage message, String channel) {
+    logger.info("message received - channel: {}, message: {}", channel, message);
+
+    String content = message.getMessage();
+    if (!Topics.APOLLO_RELEASE_TOPIC.equals(channel) || Strings.isNullOrEmpty(content)) {
+      return;
+    }
+
+    if (!watchedKeys2CacheKey.containsKey(content)) {
+      return;
+    }
+
+    //create a new list to avoid ConcurrentModificationException
+    List<String> cacheKeys = new ArrayList<>(watchedKeys2CacheKey.get(content));
+
+    for (String cacheKey : cacheKeys) {
+      logger.debug("invalidate cache key: {}", cacheKey);
+      localCache.invalidate(cacheKey);
+    }
+  }
+}

+ 9 - 66
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/controller/NotificationController.java

@@ -1,22 +1,19 @@
 package com.ctrip.framework.apollo.configservice.controller;
 
-import com.google.common.base.Joiner;
 import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.collect.HashMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Multimaps;
-import com.google.common.collect.Sets;
 
-import com.ctrip.framework.apollo.common.entity.AppNamespace;
 import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
 import com.ctrip.framework.apollo.biz.message.ReleaseMessageListener;
 import com.ctrip.framework.apollo.biz.message.Topics;
-import com.ctrip.framework.apollo.biz.service.AppNamespaceService;
 import com.ctrip.framework.apollo.biz.service.ReleaseMessageService;
 import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil;
 import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
+import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
 import com.dianping.cat.Cat;
@@ -33,7 +30,6 @@ import org.springframework.web.bind.annotation.RestController;
 import org.springframework.web.context.request.async.DeferredResult;
 
 import java.util.List;
-import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -48,12 +44,11 @@ public class NotificationController implements ReleaseMessageListener {
       deferredResults = Multimaps.synchronizedSetMultimap(HashMultimap.create());
   private static final ResponseEntity<ApolloConfigNotification>
       NOT_MODIFIED_RESPONSE = new ResponseEntity<>(HttpStatus.NOT_MODIFIED);
-  private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
   private static final Splitter STRING_SPLITTER =
       Splitter.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR).omitEmptyStrings();
 
   @Autowired
-  private AppNamespaceService appNamespaceService;
+  private WatchKeysUtil watchKeysUtil;
 
   @Autowired
   private ReleaseMessageService releaseMessageService;
@@ -75,12 +70,7 @@ public class NotificationController implements ReleaseMessageListener {
     //strip out .properties suffix
     namespace = namespaceUtil.filterNamespaceName(namespace);
 
-    Set<String> watchedKeys = assembleWatchKeys(appId, cluster, namespace, dataCenter);
-
-    //Listen on more namespaces if it's a public namespace
-    if (!namespaceBelongsToAppId(appId, namespace)) {
-      watchedKeys.addAll(this.findPublicConfigWatchKey(appId, cluster, namespace, dataCenter));
-    }
+    Set<String> watchedKeys = watchKeysUtil.assembleAllWatchKeys(appId, cluster, namespace, dataCenter);
 
     DeferredResult<ResponseEntity<ApolloConfigNotification>> deferredResult =
         new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE);
@@ -117,52 +107,13 @@ public class NotificationController implements ReleaseMessageListener {
       });
 
       logWatchedKeysToCat(watchedKeys, "Apollo.LongPoll.RegisteredKeys");
-      logger.info("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
+      logger.debug("Listening {} from appId: {}, cluster: {}, namespace: {}, datacenter: {}",
           watchedKeys, appId, cluster, namespace, dataCenter);
     }
 
     return deferredResult;
   }
 
-  private String assembleKey(String appId, String cluster, String namespace) {
-    return STRING_JOINER.join(appId, cluster, namespace);
-  }
-
-  private Set<String> findPublicConfigWatchKey(String applicationId, String clusterName,
-                                               String namespace,
-                                               String dataCenter) {
-    AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);
-
-    //check whether the namespace's appId equals to current one
-    if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
-      return Sets.newHashSet();
-    }
-
-    String publicConfigAppId = appNamespace.getAppId();
-
-    return assembleWatchKeys(publicConfigAppId, clusterName, namespace, dataCenter);
-  }
-
-  private Set<String> assembleWatchKeys(String appId, String clusterName, String namespace,
-                                        String dataCenter) {
-    Set<String> watchedKeys = Sets.newHashSet();
-
-    //watch specified cluster config change
-    if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusterName)) {
-      watchedKeys.add(assembleKey(appId, clusterName, namespace));
-    }
-
-    //watch data center config change
-    if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, clusterName)) {
-      watchedKeys.add(assembleKey(appId, dataCenter, namespace));
-    }
-
-    //watch default cluster config change
-    watchedKeys.add(assembleKey(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace));
-
-    return watchedKeys;
-  }
-
   @Override
   public void handleMessage(ReleaseMessage message, String channel) {
     logger.info("message received - channel: {}, message: {}", channel, message);
@@ -183,26 +134,18 @@ public class NotificationController implements ReleaseMessageListener {
         new ResponseEntity<>(
             new ApolloConfigNotification(keys.get(2), message.getId()), HttpStatus.OK);
 
+    if (!deferredResults.containsKey(content)) {
+      return;
+    }
     //create a new list to avoid ConcurrentModificationException
     List<DeferredResult<ResponseEntity<ApolloConfigNotification>>> results =
         Lists.newArrayList(deferredResults.get(content));
-    logger.info("Notify {} clients for key {}", results.size(), content);
+    logger.debug("Notify {} clients for key {}", results.size(), content);
 
     for (DeferredResult<ResponseEntity<ApolloConfigNotification>> result : results) {
       result.setResult(notification);
     }
-    logger.info("Notification completed");
-  }
-
-  private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
-    //Every app has an 'application' namespace
-    if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
-      return true;
-    }
-
-    AppNamespace appNamespace = appNamespaceService.findOne(appId, namespaceName);
-
-    return appNamespace != null;
+    logger.debug("Notification completed");
   }
 
   private void logWatchedKeysToCat(Set<String> watchedKeys, String eventName) {

+ 88 - 0
apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/WatchKeysUtil.java

@@ -0,0 +1,88 @@
+package com.ctrip.framework.apollo.configservice.util;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+
+import com.ctrip.framework.apollo.biz.service.AppNamespaceService;
+import com.ctrip.framework.apollo.common.entity.AppNamespace;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+@Component
+public class WatchKeysUtil {
+  private static final Joiner STRING_JOINER = Joiner.on(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR);
+  @Autowired
+  private AppNamespaceService appNamespaceService;
+
+  public Set<String> assembleAllWatchKeys(String appId, String clusterName, String namespace,
+                                          String dataCenter) {
+    Set<String> watchedKeys = assembleWatchKeys(appId, clusterName, namespace, dataCenter);
+
+    //Listen on more namespaces if it's a public namespace
+    if (!namespaceBelongsToAppId(appId, namespace)) {
+      watchedKeys.addAll(this.findPublicConfigWatchKey(appId, clusterName, namespace, dataCenter));
+    }
+
+    return watchedKeys;
+
+  }
+
+  private Set<String> findPublicConfigWatchKey(String applicationId, String clusterName,
+                                               String namespace,
+                                               String dataCenter) {
+    AppNamespace appNamespace = appNamespaceService.findPublicNamespaceByName(namespace);
+
+    //check whether the namespace's appId equals to current one
+    if (Objects.isNull(appNamespace) || Objects.equals(applicationId, appNamespace.getAppId())) {
+      return Sets.newHashSet();
+    }
+
+    String publicConfigAppId = appNamespace.getAppId();
+
+    return assembleWatchKeys(publicConfigAppId, clusterName, namespace, dataCenter);
+  }
+
+  private String assembleKey(String appId, String cluster, String namespace) {
+    return STRING_JOINER.join(appId, cluster, namespace);
+  }
+
+  private Set<String> assembleWatchKeys(String appId, String clusterName, String namespace,
+                                        String dataCenter) {
+    Set<String> watchedKeys = Sets.newHashSet();
+
+    //watch specified cluster config change
+    if (!Objects.equals(ConfigConsts.CLUSTER_NAME_DEFAULT, clusterName)) {
+      watchedKeys.add(assembleKey(appId, clusterName, namespace));
+    }
+
+    //watch data center config change
+    if (!Strings.isNullOrEmpty(dataCenter) && !Objects.equals(dataCenter, clusterName)) {
+      watchedKeys.add(assembleKey(appId, dataCenter, namespace));
+    }
+
+    //watch default cluster config change
+    watchedKeys.add(assembleKey(appId, ConfigConsts.CLUSTER_NAME_DEFAULT, namespace));
+
+    return watchedKeys;
+  }
+
+  private boolean namespaceBelongsToAppId(String appId, String namespaceName) {
+    //Every app has an 'application' namespace
+    if (Objects.equals(ConfigConsts.NAMESPACE_APPLICATION, namespaceName)) {
+      return true;
+    }
+
+    AppNamespace appNamespace = appNamespaceService.findOne(appId, namespaceName);
+
+    return appNamespace != null;
+  }
+}

+ 4 - 1
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/AllTests.java

@@ -1,8 +1,10 @@
 package com.ctrip.framework.apollo.configservice;
 
 import com.ctrip.framework.apollo.configservice.controller.ConfigControllerTest;
+import com.ctrip.framework.apollo.configservice.controller.ConfigFileControllerTest;
 import com.ctrip.framework.apollo.configservice.controller.NotificationControllerTest;
 import com.ctrip.framework.apollo.configservice.integration.ConfigControllerIntegrationTest;
+import com.ctrip.framework.apollo.configservice.integration.ConfigFileControllerIntegrationTest;
 import com.ctrip.framework.apollo.configservice.integration.NotificationControllerIntegrationTest;
 import com.ctrip.framework.apollo.configservice.util.NamespaceUtilTest;
 
@@ -13,7 +15,8 @@ import org.junit.runners.Suite.SuiteClasses;
 @RunWith(Suite.class)
 @SuiteClasses({ConfigControllerTest.class, NotificationControllerTest.class,
     ConfigControllerIntegrationTest.class, NotificationControllerIntegrationTest.class,
-    NamespaceUtilTest.class})
+    NamespaceUtilTest.class, ConfigFileControllerTest.class,
+    ConfigFileControllerIntegrationTest.class})
 public class AllTests {
 
 }

+ 164 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/ConfigFileControllerTest.java

@@ -0,0 +1,164 @@
+package com.ctrip.framework.apollo.configservice.controller;
+
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+
+import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
+import com.ctrip.framework.apollo.biz.message.Topics;
+import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
+import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
+import com.ctrip.framework.apollo.core.dto.ApolloConfig;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletResponse;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+@RunWith(MockitoJUnitRunner.class)
+public class ConfigFileControllerTest {
+  @Mock
+  private ConfigController configController;
+  @Mock
+  private WatchKeysUtil watchKeysUtil;
+  @Mock
+  private NamespaceUtil namespaceUtil;
+  private ConfigFileController configFileController;
+  private String someAppId;
+  private String someClusterName;
+  private String someNamespace;
+  private String someDataCenter;
+  private String someClientIp;
+  @Mock
+  private HttpServletResponse someResponse;
+  Multimap<String, String> watchedKeys2CacheKey;
+  Multimap<String, String> cacheKey2WatchedKeys;
+
+  @Before
+  public void setUp() throws Exception {
+    configFileController = new ConfigFileController();
+    ReflectionTestUtils.setField(configFileController, "configController", configController);
+    ReflectionTestUtils.setField(configFileController, "watchKeysUtil", watchKeysUtil);
+    ReflectionTestUtils.setField(configFileController, "namespaceUtil", namespaceUtil);
+
+    someAppId = "someAppId";
+    someClusterName = "someClusterName";
+    someNamespace = "someNamespace";
+    someDataCenter = "someDataCenter";
+    someClientIp = "10.1.1.1";
+
+    when(namespaceUtil.filterNamespaceName(someNamespace)).thenReturn(someNamespace);
+
+    watchedKeys2CacheKey =
+        (Multimap<String, String>) ReflectionTestUtils
+            .getField(configFileController, "watchedKeys2CacheKey");
+    cacheKey2WatchedKeys =
+        (Multimap<String, String>) ReflectionTestUtils
+            .getField(configFileController, "cacheKey2WatchedKeys");
+  }
+
+  @Test
+  public void testQueryConfigAsFile() throws Exception {
+    String someKey = "someKey";
+    String someValue = "someValue";
+    String anotherKey = "anotherKey";
+    String anotherValue = "anotherValue";
+
+    String someWatchKey = "someWatchKey";
+    String anotherWatchKey = "anotherWatchKey";
+    Set<String> watchKeys = Sets.newHashSet(someWatchKey, anotherWatchKey);
+
+    String cacheKey =
+        configFileController
+            .assembleCacheKey(someAppId, someClusterName, someNamespace, someDataCenter);
+
+    Map<String, String> configurations =
+        ImmutableMap.of(someKey, someValue, anotherKey, anotherValue);
+    ApolloConfig someApolloConfig = mock(ApolloConfig.class);
+    when(someApolloConfig.getConfigurations()).thenReturn(configurations);
+    when(configController
+        .queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp,
+            someResponse)).thenReturn(someApolloConfig);
+    when(watchKeysUtil
+        .assembleAllWatchKeys(someAppId, someClusterName, someNamespace, someDataCenter))
+        .thenReturn(watchKeys);
+
+    ResponseEntity<String> response =
+        configFileController
+            .queryConfigAsFile(someAppId, someClusterName, someNamespace, someDataCenter,
+                someClientIp, someResponse);
+
+    assertEquals(2, watchedKeys2CacheKey.size());
+    assertEquals(2, cacheKey2WatchedKeys.size());
+    assertTrue(watchedKeys2CacheKey.containsEntry(someWatchKey, cacheKey));
+    assertTrue(watchedKeys2CacheKey.containsEntry(anotherWatchKey, cacheKey));
+    assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, someWatchKey));
+    assertTrue(cacheKey2WatchedKeys.containsEntry(cacheKey, anotherWatchKey));
+
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertTrue(response.getBody().contains(String.format("%s=%s", someKey, someValue)));
+    assertTrue(response.getBody().contains(String.format("%s=%s", anotherKey, anotherValue)));
+
+    ResponseEntity<String> anotherResponse =
+        configFileController
+            .queryConfigAsFile(someAppId, someClusterName, someNamespace, someDataCenter,
+                someClientIp, someResponse);
+
+    assertEquals(response, anotherResponse);
+
+    verify(configController, times(1))
+        .queryConfig(someAppId, someClusterName, someNamespace, someDataCenter, "-1", someClientIp,
+            someResponse);
+  }
+
+  @Test
+  public void testHandleMessage() throws Exception {
+    String someWatchKey = "someWatchKey";
+    String anotherWatchKey = "anotherWatchKey";
+    String someCacheKey = "someCacheKey";
+    String anotherCacheKey = "anotherCacheKey";
+    String someValue = "someValue";
+
+    ReleaseMessage someReleaseMessage = mock(ReleaseMessage.class);
+    when(someReleaseMessage.getMessage()).thenReturn(someWatchKey);
+
+    Cache<String, String> cache =
+        (Cache<String, String>) ReflectionTestUtils.getField(configFileController, "localCache");
+    cache.put(someCacheKey, someValue);
+    cache.put(anotherCacheKey, someValue);
+
+    watchedKeys2CacheKey.putAll(someWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey));
+    watchedKeys2CacheKey.putAll(anotherWatchKey, Lists.newArrayList(someCacheKey, anotherCacheKey));
+
+    cacheKey2WatchedKeys.putAll(someCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey));
+    cacheKey2WatchedKeys.putAll(anotherCacheKey, Lists.newArrayList(someWatchKey, anotherWatchKey));
+
+    configFileController.handleMessage(someReleaseMessage, Topics.APOLLO_RELEASE_TOPIC);
+
+    assertTrue(watchedKeys2CacheKey.isEmpty());
+    assertTrue(cacheKey2WatchedKeys.isEmpty());
+  }
+}

+ 5 - 1
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/controller/NotificationControllerTest.java

@@ -11,6 +11,7 @@ import com.ctrip.framework.apollo.biz.service.ReleaseMessageService;
 import com.ctrip.framework.apollo.biz.utils.EntityManagerUtil;
 import com.ctrip.framework.apollo.common.entity.AppNamespace;
 import com.ctrip.framework.apollo.configservice.util.NamespaceUtil;
+import com.ctrip.framework.apollo.configservice.util.WatchKeysUtil;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.core.dto.ApolloConfigNotification;
 
@@ -61,10 +62,13 @@ public class NotificationControllerTest {
   @Before
   public void setUp() throws Exception {
     controller = new NotificationController();
-    ReflectionTestUtils.setField(controller, "appNamespaceService", appNamespaceService);
     ReflectionTestUtils.setField(controller, "releaseMessageService", releaseMessageService);
     ReflectionTestUtils.setField(controller, "entityManagerUtil", entityManagerUtil);
     ReflectionTestUtils.setField(controller, "namespaceUtil", namespaceUtil);
+    WatchKeysUtil watchKeysUtil = new WatchKeysUtil();
+    ReflectionTestUtils.setField(watchKeysUtil, "appNamespaceService", appNamespaceService);
+    ReflectionTestUtils.setField(controller, "watchKeysUtil", watchKeysUtil);
+
 
     someAppId = "someAppId";
     someCluster = "someCluster";

+ 39 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/AbstractBaseIntegrationTest.java

@@ -1,8 +1,17 @@
 package com.ctrip.framework.apollo.configservice.integration;
 
+import com.google.gson.Gson;
+
 import com.ctrip.framework.apollo.ConfigServiceTestConfiguration;
+import com.ctrip.framework.apollo.biz.entity.Namespace;
+import com.ctrip.framework.apollo.biz.entity.Release;
+import com.ctrip.framework.apollo.biz.entity.ReleaseMessage;
+import com.ctrip.framework.apollo.biz.repository.ReleaseMessageRepository;
+import com.ctrip.framework.apollo.biz.repository.ReleaseRepository;
+import com.ctrip.framework.apollo.biz.utils.ReleaseKeyGenerator;
 
 import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.test.SpringApplicationConfiguration;
 import org.springframework.boot.test.TestRestTemplate;
@@ -13,6 +22,9 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.web.client.DefaultResponseErrorHandler;
 import org.springframework.web.client.RestTemplate;
 
+import java.util.Date;
+import java.util.Map;
+
 import javax.annotation.PostConstruct;
 
 /**
@@ -22,6 +34,12 @@ import javax.annotation.PostConstruct;
 @SpringApplicationConfiguration(classes = AbstractBaseIntegrationTest.TestConfiguration.class)
 @WebIntegrationTest(randomPort = true)
 public abstract class AbstractBaseIntegrationTest {
+  @Autowired
+  private ReleaseMessageRepository releaseMessageRepository;
+  @Autowired
+  private ReleaseRepository releaseRepository;
+
+  private Gson gson = new Gson();
 
   RestTemplate restTemplate = new TestRestTemplate("user", "");
 
@@ -40,8 +58,29 @@ public abstract class AbstractBaseIntegrationTest {
   @Configuration
   @Import(ConfigServiceTestConfiguration.class)
   protected static class TestConfiguration {
+  }
+
+  protected void sendReleaseMessage(String message) {
+    ReleaseMessage releaseMessage = new ReleaseMessage(message);
+    releaseMessageRepository.save(releaseMessage);
+  }
 
+  public Release buildRelease(String name, String comment, Namespace namespace,
+                              Map<String, String> configurations, String owner) {
+    Release release = new Release();
+    release.setReleaseKey(ReleaseKeyGenerator.generateReleaseKey(namespace));
+    release.setDataChangeCreatedTime(new Date());
+    release.setDataChangeCreatedBy(owner);
+    release.setDataChangeLastModifiedBy(owner);
+    release.setName(name);
+    release.setComment(comment);
+    release.setAppId(namespace.getAppId());
+    release.setClusterName(namespace.getClusterName());
+    release.setNamespaceName(namespace.getNamespaceName());
+    release.setConfigurations(gson.toJson(configurations));
+    release = releaseRepository.save(release);
 
+    return release;
   }
 
 }

+ 128 - 0
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/ConfigFileControllerIntegrationTest.java

@@ -0,0 +1,128 @@
+package com.ctrip.framework.apollo.configservice.integration;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Lists;
+
+import com.ctrip.framework.apollo.biz.entity.Namespace;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.netflix.servo.util.Strings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.test.context.jdbc.Sql;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+public class ConfigFileControllerIntegrationTest extends AbstractBaseIntegrationTest {
+  private String someAppId;
+  private String someCluster;
+  private String someNamespace;
+  private String somePublicNamespace;
+  private String someDC;
+  private String someDefaultCluster;
+
+  @Before
+  public void setUp() throws Exception {
+    someDefaultCluster = ConfigConsts.CLUSTER_NAME_DEFAULT;
+    someAppId = "someAppId";
+    someCluster = "someCluster";
+    someNamespace = "someNamespace";
+    somePublicNamespace = "somePublicNamespace";
+    someDC = "someDC";
+  }
+
+  @Test
+  @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+  public void testQueryConfigAsFile() throws Exception {
+    ResponseEntity<String> response =
+        restTemplate
+            .getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
+                getHostUrl(), someAppId, someCluster, someNamespace);
+
+    String result = response.getBody();
+
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertTrue(result.contains("k2=v2"));
+  }
+
+  @Test
+  @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/integration-test/test-release-public-dc-override.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+  public void testQueryPublicConfigAsFile() throws Exception {
+    ResponseEntity<String> response =
+        restTemplate
+            .getForEntity(
+                "{baseurl}/configfiles/{appId}/{clusterName}/{namespace}?dataCenter={dateCenter}",
+                String.class,
+                getHostUrl(), someAppId, someDefaultCluster, somePublicNamespace, someDC);
+
+    String result = response.getBody();
+
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertTrue(result.contains("k1=override-someDC-v1"));
+    assertTrue(result.contains("k2=someDC-v2"));
+  }
+
+  @Test
+  @Sql(scripts = "/integration-test/test-release.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
+  @Sql(scripts = "/integration-test/cleanup.sql", executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD)
+  public void testConfigChanged() throws Exception {
+    ResponseEntity<String> response =
+        restTemplate
+            .getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
+                getHostUrl(), someAppId, someCluster, someNamespace);
+
+    String result = response.getBody();
+
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertTrue(result.contains("k2=v2"));
+
+    String someReleaseName = "someReleaseName";
+    String someReleaseComment = "someReleaseComment";
+    Namespace namespace = new Namespace();
+    namespace.setAppId(someAppId);
+    namespace.setClusterName(someCluster);
+    namespace.setNamespaceName(someNamespace);
+    String someOwner = "someOwner";
+
+    Map<String, String> newConfigurations = ImmutableMap.of("k1", "v1-changed", "k2", "v2-changed");
+
+    buildRelease(someReleaseName, someReleaseComment, namespace, newConfigurations, someOwner);
+
+    ResponseEntity<String> anotherResponse =
+        restTemplate
+            .getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
+                getHostUrl(), someAppId, someCluster, someNamespace);
+
+    assertEquals(response.getBody(), anotherResponse.getBody());
+
+    List<String> keys = Lists.newArrayList(someAppId, someCluster, someNamespace);
+    String message = Strings.join(ConfigConsts.CLUSTER_NAMESPACE_SEPARATOR, keys.iterator());
+    sendReleaseMessage(message);
+
+    TimeUnit.MILLISECONDS.sleep(500);
+
+    ResponseEntity<String> newResponse =
+        restTemplate
+            .getForEntity("{baseurl}/configfiles/{appId}/{clusterName}/{namespace}", String.class,
+                getHostUrl(), someAppId, someCluster, someNamespace);
+
+    result = newResponse.getBody();
+    assertEquals(HttpStatus.OK, response.getStatusCode());
+    assertTrue(result.contains("k1=v1-changed"));
+    assertTrue(result.contains("k2=v2-changed"));
+  }
+
+}

+ 2 - 4
apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/integration/NotificationControllerIntegrationTest.java

@@ -29,8 +29,7 @@ import static org.junit.Assert.assertNotEquals;
 public class NotificationControllerIntegrationTest extends AbstractBaseIntegrationTest {
   @Autowired
   private NotificationController notificationController;
-  @Autowired
-  private ReleaseMessageRepository releaseMessageRepository;
+
   private String someAppId;
   private String someCluster;
   private String defaultNamespace;
@@ -244,8 +243,7 @@ public class NotificationControllerIntegrationTest extends AbstractBaseIntegrati
           break;
         }
 
-        ReleaseMessage releaseMessage = new ReleaseMessage(message);
-        releaseMessageRepository.save(releaseMessage);
+        sendReleaseMessage(message);
       }
     });
   }

+ 1 - 1
apollo-core/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 42 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/utils/PropertiesUtil.java

@@ -0,0 +1,42 @@
+package com.ctrip.framework.apollo.core.utils;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Properties;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+public class PropertiesUtil {
+  /**
+   * Transform the properties to string format
+   * @param properties the properties object
+   * @return the string containing the properties
+   * @throws IOException
+   */
+  public static String toString(Properties properties) throws IOException {
+    StringWriter writer = new StringWriter();
+    properties.store(writer, null);
+    StringBuffer stringBuffer = writer.getBuffer();
+    filterPropertiesComment(stringBuffer);
+    return stringBuffer.toString();
+  }
+
+  /**
+   * filter out the first comment line
+   * @param stringBuffer the string buffer
+   * @return true if filtered successfully, false otherwise
+   */
+  static boolean filterPropertiesComment(StringBuffer stringBuffer) {
+    //check whether has comment in the first line
+    if (stringBuffer.charAt(0) != '#') {
+      return false;
+    }
+    int commentLineIndex = stringBuffer.indexOf("\n");
+    if (commentLineIndex == -1) {
+      return false;
+    }
+    stringBuffer.delete(0, commentLineIndex + 1);
+    return true;
+  }
+}

+ 1 - 1
apollo-demo/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<artifactId>apollo</artifactId>
 		<groupId>com.ctrip.framework.apollo</groupId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>
 	<artifactId>apollo-demo</artifactId>

+ 3 - 1
apollo-demo/src/main/java/ApolloConfigFileDemo.java

@@ -8,6 +8,8 @@ import org.slf4j.LoggerFactory;
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.util.Properties;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
@@ -18,7 +20,7 @@ public class ApolloConfigFileDemo {
   private String namespace = "application";
 
   public ApolloConfigFileDemo() {
-    configFile = ConfigService.getConfigFile(namespace, ConfigFileFormat.XML);
+    configFile = ConfigService.getConfigFile(namespace, ConfigFileFormat.Properties);
   }
 
   private void print() {

+ 1 - 1
apollo-portal/pom.xml

@@ -4,7 +4,7 @@
 	<parent>
 		<groupId>com.ctrip.framework.apollo</groupId>
 		<artifactId>apollo</artifactId>
-		<version>0.0.5</version>
+		<version>0.0.6-SNAPSHOT</version>
 		<relativePath>../pom.xml</relativePath>
 	</parent>
 	<modelVersion>4.0.0</modelVersion>

+ 1 - 1
pom.xml

@@ -4,7 +4,7 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>com.ctrip.framework.apollo</groupId>
 	<artifactId>apollo</artifactId>
-	<version>0.0.5</version>
+	<version>0.0.6-SNAPSHOT</version>
 	<name>Apollo</name>
 	<packaging>pom</packaging>
 	<description>Ctrip Configuration Center</description>