Kaynağa Gözat

add yaml support to apollo-client

1. support yaml config file transform to properties
2. support yaml config file injection to Spring
Jason Song 6 yıl önce
ebeveyn
işleme
b1ca961b60
51 değiştirilmiş dosya ile 1690 ekleme ve 38 silme
  1. 6 0
      apollo-client/pom.xml
  2. 18 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/PropertiesCompatibleConfigFile.java
  3. 2 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java
  4. 56 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesCompatibleFileConfigRepository.java
  5. 0 1
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java
  6. 60 1
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/YamlConfigFile.java
  7. 1 1
      apollo-client/src/main/java/com/ctrip/framework/apollo/internals/YmlConfigFile.java
  8. 39 3
      apollo-client/src/main/java/com/ctrip/framework/apollo/spi/DefaultConfigFactory.java
  9. 6 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java
  10. 182 0
      apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java
  11. 118 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesCompatibleFileConfigRepositoryTest.java
  12. 183 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/internals/YamlConfigFileTest.java
  13. 64 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/spi/DefaultConfigFactoryTest.java
  14. 59 6
      apollo-client/src/test/java/com/ctrip/framework/apollo/spring/AbstractSpringIntegrationTest.java
  15. 107 3
      apollo-client/src/test/java/com/ctrip/framework/apollo/spring/BootstrapConfigTest.java
  16. 76 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java
  17. 184 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigPlaceholderAutoUpdateTest.java
  18. 90 7
      apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigPlaceholderTest.java
  19. 81 0
      apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java
  20. 12 0
      apollo-client/src/test/resources/spring/XmlConfigPlaceholderTest11.xml
  21. 2 0
      apollo-client/src/test/resources/spring/yaml/case1-new.yaml
  22. 2 0
      apollo-client/src/test/resources/spring/yaml/case1.yaml
  23. 1 0
      apollo-client/src/test/resources/spring/yaml/case2-new.yml
  24. 1 0
      apollo-client/src/test/resources/spring/yaml/case2.yml
  25. 2 0
      apollo-client/src/test/resources/spring/yaml/case3-new.yaml
  26. 1 0
      apollo-client/src/test/resources/spring/yaml/case3.yaml
  27. 0 0
      apollo-client/src/test/resources/spring/yaml/case4-new.yaml
  28. 2 0
      apollo-client/src/test/resources/spring/yaml/case4.yaml
  29. 2 0
      apollo-client/src/test/resources/spring/yaml/case5-new.yaml
  30. 2 0
      apollo-client/src/test/resources/spring/yaml/case5.yaml
  31. 1 0
      apollo-client/src/test/resources/spring/yaml/case6.yml
  32. 1 0
      apollo-client/src/test/resources/spring/yaml/case7.yml
  33. 0 0
      apollo-client/src/test/resources/spring/yaml/case8.yml
  34. 1 0
      apollo-client/src/test/resources/spring/yaml/case9-new.yml
  35. 1 0
      apollo-client/src/test/resources/spring/yaml/case9.yml
  36. 25 0
      apollo-client/src/test/resources/yaml/case1.yaml
  37. 4 0
      apollo-client/src/test/resources/yaml/case2.yaml
  38. 3 0
      apollo-client/src/test/resources/yaml/case3.yaml
  39. 148 0
      apollo-client/src/test/resources/yaml/case4.yaml
  40. 42 0
      apollo-client/src/test/resources/yaml/case5.yaml
  41. 36 0
      apollo-client/src/test/resources/yaml/case6.yaml
  42. 1 0
      apollo-client/src/test/resources/yaml/case7.yaml
  43. 1 0
      apollo-client/src/test/resources/yaml/case8.yaml
  44. 4 0
      apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/ConfigFileFormat.java
  45. 38 11
      apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java
  46. 1 1
      apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/common/config/AnotherAppConfig.java
  47. 18 0
      apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/springBootDemo/config/SampleRedisConfig.java
  48. 2 1
      apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/springBootDemo/refresh/SpringBootApolloRefreshConfig.java
  49. 1 1
      apollo-demo/src/main/resources/application.yml
  50. 1 1
      apollo-demo/src/main/resources/spring.xml
  51. 2 1
      apollo-portal/src/main/resources/static/namespace.html

+ 6 - 0
apollo-client/pom.xml

@@ -45,6 +45,12 @@
 			<groupId>org.slf4j</groupId>
 			<artifactId>slf4j-api</artifactId>
 		</dependency>
+		<!-- yml processing -->
+		<dependency>
+			<groupId>org.yaml</groupId>
+			<artifactId>snakeyaml</artifactId>
+		</dependency>
+		<!-- end of yml processing -->
 		<!-- optional spring dependency -->
 		<dependency>
 			<groupId>org.springframework</groupId>

+ 18 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/PropertiesCompatibleConfigFile.java

@@ -0,0 +1,18 @@
+package com.ctrip.framework.apollo;
+
+import java.util.Properties;
+
+/**
+ * Config files that are properties compatible, e.g. yaml
+ *
+ * @since 1.3.0
+ */
+public interface PropertiesCompatibleConfigFile extends ConfigFile {
+
+  /**
+   * @return the properties form of the config file
+   *
+   * @throws RuntimeException if the content could not be transformed to properties
+   */
+  Properties asProperties();
+}

+ 2 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java

@@ -11,6 +11,7 @@ import com.ctrip.framework.apollo.tracer.Tracer;
 import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.http.HttpUtil;
 
+import com.ctrip.framework.apollo.util.yaml.YamlParser;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Singleton;
@@ -60,6 +61,7 @@ public class DefaultInjector implements Injector {
       bind(HttpUtil.class).in(Singleton.class);
       bind(ConfigServiceLocator.class).in(Singleton.class);
       bind(RemoteConfigLongPollService.class).in(Singleton.class);
+      bind(YamlParser.class).in(Singleton.class);
     }
   }
 }

+ 56 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesCompatibleFileConfigRepository.java

@@ -0,0 +1,56 @@
+package com.ctrip.framework.apollo.internals;
+
+import java.util.Properties;
+
+import com.ctrip.framework.apollo.ConfigFileChangeListener;
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
+import com.ctrip.framework.apollo.model.ConfigFileChangeEvent;
+import com.google.common.base.Preconditions;
+
+public class PropertiesCompatibleFileConfigRepository extends AbstractConfigRepository implements
+    ConfigFileChangeListener {
+  private final PropertiesCompatibleConfigFile configFile;
+  private volatile Properties cachedProperties;
+
+  public PropertiesCompatibleFileConfigRepository(PropertiesCompatibleConfigFile configFile) {
+    this.configFile = configFile;
+    this.configFile.addChangeListener(this);
+    this.trySync();
+  }
+
+  @Override
+  protected synchronized void sync() {
+    Properties current = configFile.asProperties();
+
+    Preconditions.checkState(current != null, "PropertiesCompatibleConfigFile.asProperties should never return null");
+
+    if (cachedProperties != current) {
+      cachedProperties = current;
+      this.fireRepositoryChange(configFile.getNamespace(), cachedProperties);
+    }
+  }
+
+  @Override
+  public Properties getConfig() {
+    if (cachedProperties == null) {
+      sync();
+    }
+    return cachedProperties;
+  }
+
+  @Override
+  public void setUpstreamRepository(ConfigRepository upstreamConfigRepository) {
+    //config file is the upstream, so no need to set up extra upstream
+  }
+
+  @Override
+  public ConfigSourceType getSourceType() {
+    return configFile.getSourceType();
+  }
+
+  @Override
+  public void onChange(ConfigFileChangeEvent changeEvent) {
+    this.trySync();
+  }
+}

+ 0 - 1
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/PropertiesConfigFile.java

@@ -16,7 +16,6 @@ import com.ctrip.framework.apollo.util.ExceptionUtil;
  * @author Jason Song(song_s@ctrip.com)
  */
 public class PropertiesConfigFile extends AbstractConfigFile {
-  private static final Logger logger = LoggerFactory.getLogger(PropertiesConfigFile.class);
   protected AtomicReference<String> m_contentCache;
 
   public PropertiesConfigFile(String namespace,

+ 60 - 1
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/YamlConfigFile.java

@@ -1,17 +1,76 @@
 package com.ctrip.framework.apollo.internals;
 
+import com.ctrip.framework.apollo.util.ExceptionUtil;
+import java.util.Properties;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.build.ApolloInjector;
 import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
+import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
+import com.ctrip.framework.apollo.tracer.Tracer;
+import com.ctrip.framework.apollo.util.yaml.YamlParser;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
  */
-public class YamlConfigFile extends PlainTextConfigFile {
+public class YamlConfigFile extends PlainTextConfigFile implements PropertiesCompatibleConfigFile {
+  private static final Logger logger = LoggerFactory.getLogger(YamlConfigFile.class);
+  private volatile Properties cachedProperties;
+
   public YamlConfigFile(String namespace, ConfigRepository configRepository) {
     super(namespace, configRepository);
+    tryTransformToProperties();
   }
 
   @Override
   public ConfigFileFormat getConfigFileFormat() {
     return ConfigFileFormat.YAML;
   }
+
+  @Override
+  protected void update(Properties newProperties) {
+    super.update(newProperties);
+    tryTransformToProperties();
+  }
+
+  @Override
+  public Properties asProperties() {
+    if (cachedProperties == null) {
+      transformToProperties();
+    }
+    return cachedProperties;
+  }
+
+  private boolean tryTransformToProperties() {
+    try {
+      transformToProperties();
+      return true;
+    } catch (Throwable ex) {
+      Tracer.logEvent("ApolloConfigException", ExceptionUtil.getDetailMessage(ex));
+      logger.warn("yaml to properties failed, reason: {}", ExceptionUtil.getDetailMessage(ex));
+    }
+    return false;
+  }
+
+  private synchronized void transformToProperties() {
+    cachedProperties = toProperties();
+  }
+
+  private Properties toProperties() {
+    if (!this.hasContent()) {
+      return new Properties();
+    }
+
+    try {
+      return ApolloInjector.getInstance(YamlParser.class).yamlToProperties(getContent());
+    } catch (Throwable ex) {
+      ApolloConfigException exception = new ApolloConfigException(
+          "Parse yaml file content failed for namespace: " + m_namespace, ex);
+      Tracer.logError(exception);
+      throw exception;
+    }
+  }
 }

+ 1 - 1
apollo-client/src/main/java/com/ctrip/framework/apollo/internals/YmlConfigFile.java

@@ -5,7 +5,7 @@ import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
 /**
  * @author Jason Song(song_s@ctrip.com)
  */
-public class YmlConfigFile extends PlainTextConfigFile {
+public class YmlConfigFile extends YamlConfigFile {
   public YmlConfigFile(String namespace, ConfigRepository configRepository) {
     super(namespace, configRepository);
   }

+ 39 - 3
apollo-client/src/main/java/com/ctrip/framework/apollo/spi/DefaultConfigFactory.java

@@ -1,5 +1,8 @@
 package com.ctrip.framework.apollo.spi;
 
+import com.ctrip.framework.apollo.ConfigService;
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.internals.PropertiesCompatibleFileConfigRepository;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -31,9 +34,11 @@ public class DefaultConfigFactory implements ConfigFactory {
 
   @Override
   public Config create(String namespace) {
-    DefaultConfig defaultConfig =
-        new DefaultConfig(namespace, createLocalConfigRepository(namespace));
-    return defaultConfig;
+    ConfigFileFormat format = determineFileFormat(namespace);
+    if (ConfigFileFormat.isPropertiesCompatible(format)) {
+      return new DefaultConfig(namespace, createPropertiesCompatibleFileConfigRepository(namespace, format));
+    }
+    return new DefaultConfig(namespace, createLocalConfigRepository(namespace));
   }
 
   @Override
@@ -68,4 +73,35 @@ public class DefaultConfigFactory implements ConfigFactory {
   RemoteConfigRepository createRemoteConfigRepository(String namespace) {
     return new RemoteConfigRepository(namespace);
   }
+
+  PropertiesCompatibleFileConfigRepository createPropertiesCompatibleFileConfigRepository(String namespace,
+      ConfigFileFormat format) {
+    String actualNamespaceName = trimNamespaceFormat(namespace, format);
+    PropertiesCompatibleConfigFile configFile = (PropertiesCompatibleConfigFile) ConfigService
+        .getConfigFile(actualNamespaceName, format);
+
+    return new PropertiesCompatibleFileConfigRepository(configFile);
+  }
+
+  // for namespaces whose format are not properties, the file extension must be present, e.g. application.yaml
+  ConfigFileFormat determineFileFormat(String namespaceName) {
+    String lowerCase = namespaceName.toLowerCase();
+    for (ConfigFileFormat format : ConfigFileFormat.values()) {
+      if (lowerCase.endsWith("." + format.getValue())) {
+        return format;
+      }
+    }
+
+    return ConfigFileFormat.Properties;
+  }
+
+  String trimNamespaceFormat(String namespaceName, ConfigFileFormat format) {
+    String extension = "." + format.getValue();
+    if (!namespaceName.toLowerCase().endsWith(extension)) {
+      return namespaceName;
+    }
+
+    return namespaceName.substring(0, namespaceName.length() - extension.length());
+  }
+
 }

+ 6 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/spring/config/PropertySourcesProcessor.java

@@ -137,4 +137,10 @@ public class PropertySourcesProcessor implements BeanFactoryPostProcessor, Envir
     //make it as early as possible
     return Ordered.HIGHEST_PRECEDENCE;
   }
+
+  // for test only
+  static void reset() {
+    NAMESPACE_NAMES.clear();
+    AUTO_UPDATE_INITIALIZED_BEAN_FACTORIES.clear();
+  }
 }

+ 182 - 0
apollo-client/src/main/java/com/ctrip/framework/apollo/util/yaml/YamlParser.java

@@ -0,0 +1,182 @@
+package com.ctrip.framework.apollo.util.yaml;
+
+import java.util.AbstractMap;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.yaml.snakeyaml.Yaml;
+import org.yaml.snakeyaml.constructor.Constructor;
+import org.yaml.snakeyaml.nodes.MappingNode;
+import org.yaml.snakeyaml.parser.ParserException;
+
+import com.ctrip.framework.apollo.core.utils.StringUtils;
+
+/**
+ * Transplanted from org.springframework.beans.factory.config.YamlProcessor since apollo can't depend on Spring directly
+ *
+ * @since 1.3.0
+ */
+public class YamlParser {
+  private static final Logger logger = LoggerFactory.getLogger(YamlParser.class);
+
+  /**
+   * Transform yaml content to properties
+   */
+  public Properties yamlToProperties(String yamlContent) {
+    Yaml yaml = createYaml();
+    final Properties result = new Properties();
+    process(new MatchCallback() {
+      @Override
+      public void process(Properties properties, Map<String, Object> map) {
+        result.putAll(properties);
+      }
+    }, yaml, yamlContent);
+    return result;
+  }
+
+  /**
+   * Create the {@link Yaml} instance to use.
+   */
+  private Yaml createYaml() {
+    return new Yaml(new StrictMapAppenderConstructor());
+  }
+
+  private boolean process(MatchCallback callback, Yaml yaml, String content) {
+    int count = 0;
+    if (logger.isDebugEnabled()) {
+      logger.debug("Loading from YAML: " + content);
+    }
+    for (Object object : yaml.loadAll(content)) {
+      if (object != null && process(asMap(object), callback)) {
+        count++;
+      }
+    }
+    if (logger.isDebugEnabled()) {
+      logger.debug("Loaded " + count + " document" + (count > 1 ? "s" : "") + " from YAML resource: " + content);
+    }
+    return (count > 0);
+  }
+
+  @SuppressWarnings("unchecked")
+  private Map<String, Object> asMap(Object object) {
+    // YAML can have numbers as keys
+    Map<String, Object> result = new LinkedHashMap<String, Object>();
+    if (!(object instanceof Map)) {
+      // A document can be a text literal
+      result.put("document", object);
+      return result;
+    }
+
+    Map<Object, Object> map = (Map<Object, Object>) object;
+    for (Map.Entry<Object, Object> entry : map.entrySet()) {
+      Object value = entry.getValue();
+      if (value instanceof Map) {
+        value = asMap(value);
+      }
+      Object key = entry.getKey();
+      if (key instanceof CharSequence) {
+        result.put(key.toString(), value);
+      } else {
+        // It has to be a map key in this case
+        result.put("[" + key.toString() + "]", value);
+      }
+    }
+    return result;
+  }
+
+  private boolean process(Map<String, Object> map, MatchCallback callback) {
+    Properties properties = new Properties();
+    properties.putAll(getFlattenedMap(map));
+
+    if (logger.isDebugEnabled()) {
+      logger.debug("Merging document (no matchers set): " + map);
+    }
+    callback.process(properties, map);
+    return true;
+  }
+
+  private Map<String, Object> getFlattenedMap(Map<String, Object> source) {
+    Map<String, Object> result = new LinkedHashMap<String, Object>();
+    buildFlattenedMap(result, source, null);
+    return result;
+  }
+
+  private void buildFlattenedMap(Map<String, Object> result, Map<String, Object> source, String path) {
+    for (Map.Entry<String, Object> entry : source.entrySet()) {
+      String key = entry.getKey();
+      if (!StringUtils.isBlank(path)) {
+        if (key.startsWith("[")) {
+          key = path + key;
+        } else {
+          key = path + '.' + key;
+        }
+      }
+      Object value = entry.getValue();
+      if (value instanceof String) {
+        result.put(key, value);
+      } else if (value instanceof Map) {
+        // Need a compound key
+        @SuppressWarnings("unchecked")
+        Map<String, Object> map = (Map<String, Object>) value;
+        buildFlattenedMap(result, map, key);
+      } else if (value instanceof Collection) {
+        // Need a compound key
+        @SuppressWarnings("unchecked")
+        Collection<Object> collection = (Collection<Object>) value;
+        int count = 0;
+        for (Object object : collection) {
+          buildFlattenedMap(result, Collections.singletonMap("[" + (count++) + "]", object), key);
+        }
+      } else {
+        result.put(key, (value != null ? value.toString() : ""));
+      }
+    }
+  }
+
+  private interface MatchCallback {
+    void process(Properties properties, Map<String, Object> map);
+  }
+
+  private static class StrictMapAppenderConstructor extends Constructor {
+
+    // Declared as public for use in subclasses
+    StrictMapAppenderConstructor() {
+      super();
+    }
+
+    @Override
+    protected Map<Object, Object> constructMapping(MappingNode node) {
+      try {
+        return super.constructMapping(node);
+      } catch (IllegalStateException ex) {
+        throw new ParserException("while parsing MappingNode", node.getStartMark(), ex.getMessage(), node.getEndMark());
+      }
+    }
+
+    @Override
+    protected Map<Object, Object> createDefaultMap() {
+      final Map<Object, Object> delegate = super.createDefaultMap();
+      return new AbstractMap<Object, Object>() {
+        @Override
+        public Object put(Object key, Object value) {
+          if (delegate.containsKey(key)) {
+            throw new IllegalStateException("Duplicate key: " + key);
+          }
+          return delegate.put(key, value);
+        }
+
+        @Override
+        public Set<Entry<Object, Object>> entrySet() {
+          return delegate.entrySet();
+        }
+      };
+    }
+  }
+
+}

+ 118 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/PropertiesCompatibleFileConfigRepositoryTest.java

@@ -0,0 +1,118 @@
+package com.ctrip.framework.apollo.internals;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
+import com.ctrip.framework.apollo.model.ConfigFileChangeEvent;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class PropertiesCompatibleFileConfigRepositoryTest {
+
+  @Mock
+  private PropertiesCompatibleConfigFile configFile;
+
+  private String someNamespaceName;
+
+  @Mock
+  private Properties someProperties;
+
+  @Before
+  public void setUp() throws Exception {
+    someNamespaceName = "someNamespaceName";
+    when(configFile.getNamespace()).thenReturn(someNamespaceName);
+    when(configFile.asProperties()).thenReturn(someProperties);
+  }
+
+  @Test
+  public void testGetConfig() throws Exception {
+    PropertiesCompatibleFileConfigRepository configFileRepository = new PropertiesCompatibleFileConfigRepository(
+        configFile);
+
+    assertSame(someProperties, configFileRepository.getConfig());
+    verify(configFile, times(1)).addChangeListener(configFileRepository);
+  }
+
+  @Test
+  public void testGetConfigFailedAndThenRecovered() throws Exception {
+    RuntimeException someException = new RuntimeException("some exception");
+
+    when(configFile.asProperties()).thenThrow(someException);
+
+    PropertiesCompatibleFileConfigRepository configFileRepository = new PropertiesCompatibleFileConfigRepository(
+        configFile);
+
+    Throwable exceptionThrown = null;
+    try {
+      configFileRepository.getConfig();
+    } catch (Throwable ex) {
+      exceptionThrown = ex;
+    }
+
+    assertSame(someException, exceptionThrown);
+
+    // recovered
+    reset(configFile);
+
+    Properties someProperties = mock(Properties.class);
+
+    when(configFile.asProperties()).thenReturn(someProperties);
+
+    assertSame(someProperties, configFileRepository.getConfig());
+  }
+
+  @Test(expected = IllegalStateException.class)
+  public void testGetConfigWithConfigFileReturnNullProperties() throws Exception {
+    when(configFile.asProperties()).thenReturn(null);
+
+    PropertiesCompatibleFileConfigRepository configFileRepository = new PropertiesCompatibleFileConfigRepository(
+        configFile);
+
+    configFileRepository.getConfig();
+  }
+
+  @Test
+  public void testGetSourceType() throws Exception {
+    ConfigSourceType someType = ConfigSourceType.REMOTE;
+
+    when(configFile.getSourceType()).thenReturn(someType);
+
+    PropertiesCompatibleFileConfigRepository configFileRepository = new PropertiesCompatibleFileConfigRepository(
+        configFile);
+
+    assertSame(someType, configFileRepository.getSourceType());
+  }
+
+  @Test
+  public void testOnChange() throws Exception {
+    Properties anotherProperties = mock(Properties.class);
+    ConfigFileChangeEvent someChangeEvent = mock(ConfigFileChangeEvent.class);
+
+    RepositoryChangeListener someListener = mock(RepositoryChangeListener.class);
+
+    PropertiesCompatibleFileConfigRepository configFileRepository = new PropertiesCompatibleFileConfigRepository(
+        configFile);
+
+    configFileRepository.addChangeListener(someListener);
+
+    assertSame(someProperties, configFileRepository.getConfig());
+
+    when(configFile.asProperties()).thenReturn(anotherProperties);
+
+    configFileRepository.onChange(someChangeEvent);
+
+    assertSame(anotherProperties, configFileRepository.getConfig());
+    verify(someListener, times(1)).onRepositoryChange(someNamespaceName, anotherProperties);
+  }
+}

+ 183 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/internals/YamlConfigFileTest.java

@@ -0,0 +1,183 @@
+package com.ctrip.framework.apollo.internals;
+
+import static org.junit.Assert.*;
+import static org.mockito.Mockito.when;
+
+import com.ctrip.framework.apollo.build.MockInjector;
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.enums.ConfigSourceType;
+import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
+import com.ctrip.framework.apollo.util.yaml.YamlParser;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.runners.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public class YamlConfigFileTest {
+  private String someNamespace;
+  @Mock
+  private ConfigRepository configRepository;
+  @Mock
+  private YamlParser yamlParser;
+
+  private ConfigSourceType someSourceType;
+
+  @Before
+  public void setUp() throws Exception {
+    someNamespace = "someName";
+
+    MockInjector.reset();
+    MockInjector.setInstance(YamlParser.class, yamlParser);
+  }
+
+  @Test
+  public void testWhenHasContent() throws Exception {
+    Properties someProperties = new Properties();
+    String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY;
+    String someContent = "someKey: 'someValue'";
+    someProperties.setProperty(key, someContent);
+    someSourceType = ConfigSourceType.LOCAL;
+
+    Properties yamlProperties = new Properties();
+    yamlProperties.setProperty("someKey", "someValue");
+
+    when(configRepository.getConfig()).thenReturn(someProperties);
+    when(configRepository.getSourceType()).thenReturn(someSourceType);
+    when(yamlParser.yamlToProperties(someContent)).thenReturn(yamlProperties);
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertSame(someContent, configFile.getContent());
+    assertSame(yamlProperties, configFile.asProperties());
+  }
+
+  @Test
+  public void testWhenHasNoContent() throws Exception {
+    when(configRepository.getConfig()).thenReturn(null);
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertFalse(configFile.hasContent());
+    assertNull(configFile.getContent());
+
+    Properties properties = configFile.asProperties();
+
+    assertTrue(properties.isEmpty());
+  }
+
+  @Test
+  public void testWhenInvalidYamlContent() throws Exception {
+    Properties someProperties = new Properties();
+    String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY;
+    String someInvalidContent = ",";
+    someProperties.setProperty(key, someInvalidContent);
+    someSourceType = ConfigSourceType.LOCAL;
+
+    when(configRepository.getConfig()).thenReturn(someProperties);
+    when(configRepository.getSourceType()).thenReturn(someSourceType);
+    when(yamlParser.yamlToProperties(someInvalidContent)).thenThrow(new RuntimeException("some exception"));
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertSame(someInvalidContent, configFile.getContent());
+
+    Throwable exceptionThrown = null;
+    try {
+      configFile.asProperties();
+    } catch (Throwable ex) {
+      exceptionThrown = ex;
+    }
+
+    assertTrue(exceptionThrown instanceof ApolloConfigException);
+    assertNotNull(exceptionThrown.getCause());
+  }
+
+  @Test
+  public void testWhenConfigRepositoryHasError() throws Exception {
+    when(configRepository.getConfig()).thenThrow(new RuntimeException("someError"));
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertFalse(configFile.hasContent());
+    assertNull(configFile.getContent());
+    assertEquals(ConfigSourceType.NONE, configFile.getSourceType());
+
+    Properties properties = configFile.asProperties();
+
+    assertTrue(properties.isEmpty());
+  }
+
+  @Test
+  public void testOnRepositoryChange() throws Exception {
+    Properties someProperties = new Properties();
+    String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY;
+    String someValue = "someKey: 'someValue'";
+    String anotherValue = "anotherKey: 'anotherValue'";
+    someProperties.setProperty(key, someValue);
+
+    someSourceType = ConfigSourceType.LOCAL;
+
+    Properties someYamlProperties = new Properties();
+    someYamlProperties.setProperty("someKey", "someValue");
+
+    Properties anotherYamlProperties = new Properties();
+    anotherYamlProperties.setProperty("anotherKey", "anotherValue");
+
+    when(configRepository.getConfig()).thenReturn(someProperties);
+    when(configRepository.getSourceType()).thenReturn(someSourceType);
+    when(yamlParser.yamlToProperties(someValue)).thenReturn(someYamlProperties);
+    when(yamlParser.yamlToProperties(anotherValue)).thenReturn(anotherYamlProperties);
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertEquals(someValue, configFile.getContent());
+    assertEquals(someSourceType, configFile.getSourceType());
+    assertSame(someYamlProperties, configFile.asProperties());
+
+    Properties anotherProperties = new Properties();
+    anotherProperties.setProperty(key, anotherValue);
+
+    ConfigSourceType anotherSourceType = ConfigSourceType.REMOTE;
+    when(configRepository.getSourceType()).thenReturn(anotherSourceType);
+
+    configFile.onRepositoryChange(someNamespace, anotherProperties);
+
+    assertEquals(anotherValue, configFile.getContent());
+    assertEquals(anotherSourceType, configFile.getSourceType());
+    assertSame(anotherYamlProperties, configFile.asProperties());
+  }
+
+  @Test
+  public void testWhenConfigRepositoryHasErrorAndThenRecovered() throws Exception {
+    Properties someProperties = new Properties();
+    String key = ConfigConsts.CONFIG_FILE_CONTENT_KEY;
+    String someValue = "someKey: 'someValue'";
+    someProperties.setProperty(key, someValue);
+
+    someSourceType = ConfigSourceType.LOCAL;
+
+    Properties someYamlProperties = new Properties();
+    someYamlProperties.setProperty("someKey", "someValue");
+
+    when(configRepository.getConfig()).thenThrow(new RuntimeException("someError"));
+    when(configRepository.getSourceType()).thenReturn(someSourceType);
+    when(yamlParser.yamlToProperties(someValue)).thenReturn(someYamlProperties);
+
+    YamlConfigFile configFile = new YamlConfigFile(someNamespace, configRepository);
+
+    assertFalse(configFile.hasContent());
+    assertNull(configFile.getContent());
+    assertEquals(ConfigSourceType.NONE, configFile.getSourceType());
+    assertTrue(configFile.asProperties().isEmpty());
+
+    configFile.onRepositoryChange(someNamespace, someProperties);
+
+    assertTrue(configFile.hasContent());
+    assertEquals(someValue, configFile.getContent());
+    assertEquals(someSourceType, configFile.getSourceType());
+    assertSame(someYamlProperties, configFile.asProperties());
+  }
+}

+ 64 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/spi/DefaultConfigFactoryTest.java

@@ -10,6 +10,7 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.spy;
 import static org.mockito.Mockito.when;
 
+import com.ctrip.framework.apollo.internals.PropertiesCompatibleFileConfigRepository;
 import java.util.Properties;
 
 import org.junit.Before;
@@ -78,6 +79,28 @@ public class DefaultConfigFactoryTest {
     assertNull(ReflectionTestUtils.getField(localFileConfigRepository, "m_upstream"));
   }
 
+  @Test
+  public void testCreatePropertiesCompatibleFileConfigRepository() throws Exception {
+    ConfigFileFormat somePropertiesCompatibleFormat = ConfigFileFormat.YML;
+    String someNamespace = "someName" + "." + somePropertiesCompatibleFormat;
+    Properties someProperties = new Properties();
+    String someKey = "someKey";
+    String someValue = "someValue";
+    someProperties.setProperty(someKey, someValue);
+
+    PropertiesCompatibleFileConfigRepository someRepository = mock(PropertiesCompatibleFileConfigRepository.class);
+    when(someRepository.getConfig()).thenReturn(someProperties);
+
+    doReturn(someRepository).when(defaultConfigFactory)
+        .createPropertiesCompatibleFileConfigRepository(someNamespace, somePropertiesCompatibleFormat);
+
+    Config result = defaultConfigFactory.create(someNamespace);
+
+    assertThat("DefaultConfigFactory should create DefaultConfig", result,
+        is(instanceOf(DefaultConfig.class)));
+    assertEquals(someValue, result.getProperty(someKey, null));
+  }
+
   @Test
   public void testCreateConfigFile() throws Exception {
     String someNamespace = "someName";
@@ -125,6 +148,47 @@ public class DefaultConfigFactoryTest {
 
   }
 
+  @Test
+  public void testDetermineFileFormat() throws Exception {
+    checkFileFormat("abc", ConfigFileFormat.Properties);
+    checkFileFormat("abc.properties", ConfigFileFormat.Properties);
+    checkFileFormat("abc.pRopErties", ConfigFileFormat.Properties);
+    checkFileFormat("abc.xml", ConfigFileFormat.XML);
+    checkFileFormat("abc.xmL", ConfigFileFormat.XML);
+    checkFileFormat("abc.json", ConfigFileFormat.JSON);
+    checkFileFormat("abc.jsOn", ConfigFileFormat.JSON);
+    checkFileFormat("abc.yaml", ConfigFileFormat.YAML);
+    checkFileFormat("abc.yAml", ConfigFileFormat.YAML);
+    checkFileFormat("abc.yml", ConfigFileFormat.YML);
+    checkFileFormat("abc.yMl", ConfigFileFormat.YML);
+    checkFileFormat("abc.properties.yml", ConfigFileFormat.YML);
+  }
+
+  @Test
+  public void testTrimNamespaceFormat() throws Exception {
+    checkNamespaceName("abc", ConfigFileFormat.Properties, "abc");
+    checkNamespaceName("abc.properties", ConfigFileFormat.Properties, "abc");
+    checkNamespaceName("abcproperties", ConfigFileFormat.Properties, "abcproperties");
+    checkNamespaceName("abc.pRopErties", ConfigFileFormat.Properties, "abc");
+    checkNamespaceName("abc.xml", ConfigFileFormat.XML, "abc");
+    checkNamespaceName("abc.xmL", ConfigFileFormat.XML, "abc");
+    checkNamespaceName("abc.json", ConfigFileFormat.JSON, "abc");
+    checkNamespaceName("abc.jsOn", ConfigFileFormat.JSON, "abc");
+    checkNamespaceName("abc.yaml", ConfigFileFormat.YAML, "abc");
+    checkNamespaceName("abc.yAml", ConfigFileFormat.YAML, "abc");
+    checkNamespaceName("abc.yml", ConfigFileFormat.YML, "abc");
+    checkNamespaceName("abc.yMl", ConfigFileFormat.YML, "abc");
+    checkNamespaceName("abc.proPerties.yml", ConfigFileFormat.YML, "abc.proPerties");
+  }
+
+  private void checkFileFormat(String namespaceName, ConfigFileFormat expectedFormat) {
+    assertEquals(expectedFormat, defaultConfigFactory.determineFileFormat(namespaceName));
+  }
+
+  private void checkNamespaceName(String namespaceName, ConfigFileFormat format, String expectedNamespaceName) {
+    assertEquals(expectedNamespaceName, defaultConfigFactory.trimNamespaceFormat(namespaceName, format));
+  }
+
   public static class MockConfigUtil extends ConfigUtil {
     @Override
     public String getAppId() {

+ 59 - 6
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/AbstractSpringIntegrationTest.java

@@ -7,8 +7,13 @@ import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.internals.ConfigRepository;
 import com.ctrip.framework.apollo.internals.DefaultInjector;
 import com.ctrip.framework.apollo.internals.SimpleConfig;
-import com.ctrip.framework.apollo.spring.property.SpringValueDefinitionProcessor;
+import com.ctrip.framework.apollo.internals.YamlConfigFile;
+import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
 import com.ctrip.framework.apollo.util.ConfigUtil;
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+import java.io.File;
+import java.io.IOException;
 import java.lang.reflect.Method;
 import java.util.Calendar;
 import java.util.Date;
@@ -25,7 +30,6 @@ import com.ctrip.framework.apollo.ConfigService;
 import com.ctrip.framework.apollo.build.MockInjector;
 import com.ctrip.framework.apollo.core.enums.ConfigFileFormat;
 import com.ctrip.framework.apollo.internals.ConfigManager;
-import com.ctrip.framework.apollo.spring.config.PropertySourcesProcessor;
 import com.google.common.collect.Maps;
 
 /**
@@ -33,12 +37,16 @@ import com.google.common.collect.Maps;
  */
 public abstract class AbstractSpringIntegrationTest {
   private static final Map<String, Config> CONFIG_REGISTRY = Maps.newHashMap();
+  private static final Map<String, ConfigFile> CONFIG_FILE_REGISTRY = Maps.newHashMap();
   private static Method CONFIG_SERVICE_RESET;
+  private static Method PROPERTY_SOURCES_PROCESSOR_RESET;
 
   static {
     try {
       CONFIG_SERVICE_RESET = ConfigService.class.getDeclaredMethod("reset");
       ReflectionUtils.makeAccessible(CONFIG_SERVICE_RESET);
+      PROPERTY_SOURCES_PROCESSOR_RESET = PropertySourcesProcessor.class.getDeclaredMethod("reset");
+      ReflectionUtils.makeAccessible(PROPERTY_SOURCES_PROCESSOR_RESET);
     } catch (NoSuchMethodException e) {
       e.printStackTrace();
     }
@@ -66,6 +74,29 @@ public abstract class AbstractSpringIntegrationTest {
     return config;
   }
 
+  protected static Properties readYamlContentAsConfigFileProperties(String caseName) throws IOException {
+    File file = new File("src/test/resources/spring/yaml/" + caseName);
+
+    String yamlContent = Files.toString(file, Charsets.UTF_8);
+
+    Properties properties = new Properties();
+    properties.setProperty(ConfigConsts.CONFIG_FILE_CONTENT_KEY, yamlContent);
+
+    return properties;
+  }
+
+  protected static YamlConfigFile prepareYamlConfigFile(String namespaceNameWithFormat, Properties properties) {
+    ConfigRepository configRepository = mock(ConfigRepository.class);
+
+    when(configRepository.getConfig()).thenReturn(properties);
+
+    YamlConfigFile configFile = new YamlConfigFile(namespaceNameWithFormat, configRepository);
+
+    mockConfigFile(namespaceNameWithFormat, configFile);
+
+    return configFile;
+  }
+
   protected Properties assembleProperties(String key, String value) {
     Properties properties = new Properties();
     properties.setProperty(key, value);
@@ -105,12 +136,20 @@ public abstract class AbstractSpringIntegrationTest {
     CONFIG_REGISTRY.put(namespace, config);
   }
 
+  protected static void mockConfigFile(String namespaceNameWithFormat, ConfigFile configFile) {
+    CONFIG_FILE_REGISTRY.put(namespaceNameWithFormat, configFile);
+  }
+
   protected static void doSetUp() {
     //as ConfigService is singleton, so we must manually clear its container
     ReflectionUtils.invokeMethod(CONFIG_SERVICE_RESET, null);
+    //as PropertySourcesProcessor has some static variables, so we must manually clear them
+    ReflectionUtils.invokeMethod(PROPERTY_SOURCES_PROCESSOR_RESET, null);
+    DefaultInjector defaultInjector = new DefaultInjector();
+    ConfigManager defaultConfigManager = defaultInjector.getInstance(ConfigManager.class);
     MockInjector.reset();
-    MockInjector.setInstance(ConfigManager.class, new MockConfigManager());
-    MockInjector.setDelegate(new DefaultInjector());
+    MockInjector.setInstance(ConfigManager.class, new MockConfigManager(defaultConfigManager));
+    MockInjector.setDelegate(defaultInjector);
   }
 
   protected static void doTearDown() {
@@ -119,14 +158,28 @@ public abstract class AbstractSpringIntegrationTest {
 
   private static class MockConfigManager implements ConfigManager {
 
+    private final ConfigManager delegate;
+
+    public MockConfigManager(ConfigManager delegate) {
+      this.delegate = delegate;
+    }
+
     @Override
     public Config getConfig(String namespace) {
-      return CONFIG_REGISTRY.get(namespace);
+      Config config = CONFIG_REGISTRY.get(namespace);
+      if (config != null) {
+        return config;
+      }
+      return delegate.getConfig(namespace);
     }
 
     @Override
     public ConfigFile getConfigFile(String namespace, ConfigFileFormat configFileFormat) {
-      return null;
+      ConfigFile configFile = CONFIG_FILE_REGISTRY.get(String.format("%s.%s", namespace, configFileFormat.getValue()));
+      if (configFile != null) {
+        return configFile;
+      }
+      return delegate.getConfigFile(namespace, configFileFormat);
     }
   }
 

+ 107 - 3
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/BootstrapConfigTest.java

@@ -138,6 +138,45 @@ public class BootstrapConfigTest {
     }
   }
 
+  @RunWith(SpringJUnit4ClassRunner.class)
+  @SpringBootTest(classes = ConfigurationWithConditionalOnProperty.class)
+  @DirtiesContext
+  public static class TestWithBootstrapEnabledAndNamespacesAndConditionalOnWithYamlFile extends
+      AbstractSpringIntegrationTest {
+
+    @Autowired(required = false)
+    private TestBean testBean;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+      doSetUp();
+
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES,
+          String.format("%s, %s", "application.yml", FX_APOLLO_NAMESPACE));
+
+      prepareYamlConfigFile("application.yml", readYamlContentAsConfigFileProperties("case6.yml"));
+      Config anotherConfig = mock(Config.class);
+
+      mockConfig(ConfigConsts.NAMESPACE_APPLICATION, anotherConfig);
+      mockConfig(FX_APOLLO_NAMESPACE, anotherConfig);
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES);
+
+      doTearDown();
+    }
+
+    @Test
+    public void test() throws Exception {
+      Assert.assertNotNull(testBean);
+      Assert.assertTrue(testBean.execute());
+    }
+  }
+
   @RunWith(SpringJUnit4ClassRunner.class)
   @SpringBootTest(classes = ConfigurationWithConditionalOnProperty.class)
   @DirtiesContext
@@ -174,6 +213,39 @@ public class BootstrapConfigTest {
     }
   }
 
+  @RunWith(SpringJUnit4ClassRunner.class)
+  @SpringBootTest(classes = ConfigurationWithConditionalOnProperty.class)
+  @DirtiesContext
+  public static class TestWithBootstrapEnabledAndDefaultNamespacesAndConditionalOnFailedWithYamlFile extends
+      AbstractSpringIntegrationTest {
+
+    @Autowired(required = false)
+    private TestBean testBean;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+      doSetUp();
+
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, "application.yml");
+
+      prepareYamlConfigFile("application.yml", readYamlContentAsConfigFileProperties("case7.yml"));
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES);
+
+      doTearDown();
+    }
+
+    @Test
+    public void test() throws Exception {
+      Assert.assertNull(testBean);
+    }
+  }
+
   @RunWith(SpringJUnit4ClassRunner.class)
   @SpringBootTest(classes = ConfigurationWithoutConditionalOnProperty.class)
   @DirtiesContext
@@ -208,6 +280,40 @@ public class BootstrapConfigTest {
     }
   }
 
+  @RunWith(SpringJUnit4ClassRunner.class)
+  @SpringBootTest(classes = ConfigurationWithoutConditionalOnProperty.class)
+  @DirtiesContext
+  public static class TestWithBootstrapEnabledAndDefaultNamespacesAndConditionalOffWithYamlFile extends
+      AbstractSpringIntegrationTest {
+
+    @Autowired(required = false)
+    private TestBean testBean;
+
+    @BeforeClass
+    public static void beforeClass() throws Exception {
+      doSetUp();
+
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED, "true");
+      System.setProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, "application.yml");
+
+      prepareYamlConfigFile("application.yml", readYamlContentAsConfigFileProperties("case8.yml"));
+    }
+
+    @AfterClass
+    public static void afterClass() throws Exception {
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_ENABLED);
+      System.clearProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES);
+
+      doTearDown();
+    }
+
+    @Test
+    public void test() throws Exception {
+      Assert.assertNotNull(testBean);
+      Assert.assertTrue(testBean.execute());
+    }
+  }
+
   @RunWith(SpringJUnit4ClassRunner.class)
   @SpringBootTest(classes = ConfigurationWithConditionalOnProperty.class)
   @DirtiesContext
@@ -272,7 +378,6 @@ public class BootstrapConfigTest {
     }
   }
 
-
   @RunWith(SpringJUnit4ClassRunner.class)
   @SpringBootTest(classes = ConfigurationWithoutConditionalOnProperty.class)
   @DirtiesContext
@@ -306,14 +411,13 @@ public class BootstrapConfigTest {
       Boolean containsApollo = !Collections2.filter(processorList, new Predicate<EnvironmentPostProcessor>() {
             @Override
             public boolean apply(EnvironmentPostProcessor input) {
-                return  input instanceof ApolloApplicationContextInitializer;
+                return input instanceof ApolloApplicationContextInitializer;
             }
         }).isEmpty();
       Assert.assertTrue(containsApollo);
     }
   }
 
-
   @EnableAutoConfiguration
   @Configuration
   static class ConfigurationWithoutConditionalOnProperty {

+ 76 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigAnnotationTest.java

@@ -3,12 +3,16 @@ package com.ctrip.framework.apollo.spring;
 import com.ctrip.framework.apollo.Config;
 import com.ctrip.framework.apollo.ConfigChangeListener;
 import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.internals.YamlConfigFile;
+import com.ctrip.framework.apollo.model.ConfigChange;
 import com.ctrip.framework.apollo.model.ConfigChangeEvent;
 import com.ctrip.framework.apollo.spring.annotation.ApolloConfig;
 import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
 import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.SettableFuture;
+import java.util.concurrent.TimeUnit;
 import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.mockito.invocation.InvocationOnMock;
@@ -23,6 +27,7 @@ import java.util.Set;
 
 import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
 import static org.mockito.Matchers.any;
 import static org.mockito.Matchers.anySetOf;
 import static org.mockito.Mockito.doAnswer;
@@ -35,20 +40,28 @@ import static org.mockito.Mockito.verify;
  */
 public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
   private static final String FX_APOLLO_NAMESPACE = "FX.apollo";
+  private static final String APPLICATION_YAML_NAMESPACE = "application.yaml";
 
   @Test
   public void testApolloConfig() throws Exception {
     Config applicationConfig = mock(Config.class);
     Config fxApolloConfig = mock(Config.class);
+    String someKey = "someKey";
+    String someValue = "someValue";
 
     mockConfig(ConfigConsts.NAMESPACE_APPLICATION, applicationConfig);
     mockConfig(FX_APOLLO_NAMESPACE, fxApolloConfig);
 
+    prepareYamlConfigFile(APPLICATION_YAML_NAMESPACE, readYamlContentAsConfigFileProperties("case9.yml"));
+
     TestApolloConfigBean1 bean = getBean(TestApolloConfigBean1.class, AppConfig1.class);
 
     assertEquals(applicationConfig, bean.getConfig());
     assertEquals(applicationConfig, bean.getAnotherConfig());
     assertEquals(fxApolloConfig, bean.getYetAnotherConfig());
+
+    Config yamlConfig = bean.getYamlConfig();
+    assertEquals(someValue, yamlConfig.getProperty(someKey, null));
   }
 
   @Test(expected = BeanCreationException.class)
@@ -239,6 +252,33 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     assertEquals(asList(Sets.newHashSet("anotherKey")), fxApolloConfigInterestedKeys.getAllValues());
   }
 
+  @Test
+  public void testApolloConfigChangeListenerWithYamlFile() throws Exception {
+    String someKey = "someKey";
+    String someValue = "someValue";
+    String anotherValue = "anotherValue";
+
+    YamlConfigFile configFile = prepareYamlConfigFile(APPLICATION_YAML_NAMESPACE,
+        readYamlContentAsConfigFileProperties("case9.yml"));
+
+    TestApolloConfigChangeListenerWithYamlFile bean = getBean(TestApolloConfigChangeListenerWithYamlFile.class, AppConfig9.class);
+
+    Config yamlConfig = bean.getYamlConfig();
+    SettableFuture<ConfigChangeEvent> future = bean.getConfigChangeEventFuture();
+
+    assertEquals(someValue, yamlConfig.getProperty(someKey, null));
+    assertFalse(future.isDone());
+
+    configFile.onRepositoryChange(APPLICATION_YAML_NAMESPACE, readYamlContentAsConfigFileProperties("case9-new.yml"));
+
+    ConfigChangeEvent configChangeEvent = future.get(100, TimeUnit.MILLISECONDS);
+    ConfigChange change = configChangeEvent.getChange(someKey);
+    assertEquals(someValue, change.getOldValue());
+    assertEquals(anotherValue, change.getNewValue());
+
+    assertEquals(anotherValue, yamlConfig.getProperty(someKey, null));
+  }
+
   private <T> T getBean(Class<T> beanClass, Class<?>... annotatedClasses) {
     AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(annotatedClasses);
 
@@ -317,6 +357,15 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     }
   }
 
+  @Configuration
+  @EnableApolloConfig(APPLICATION_YAML_NAMESPACE)
+  static class AppConfig9 {
+    @Bean
+    public TestApolloConfigChangeListenerWithYamlFile bean() {
+      return new TestApolloConfigChangeListenerWithYamlFile();
+    }
+  }
+
   static class TestApolloConfigBean1 {
     @ApolloConfig
     private Config config;
@@ -324,6 +373,8 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     private Config anotherConfig;
     @ApolloConfig(FX_APOLLO_NAMESPACE)
     private Config yetAnotherConfig;
+    @ApolloConfig(APPLICATION_YAML_NAMESPACE)
+    private Config yamlConfig;
 
     public Config getConfig() {
       return config;
@@ -336,6 +387,10 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
     public Config getYetAnotherConfig() {
       return yetAnotherConfig;
     }
+
+    public Config getYamlConfig() {
+      return yamlConfig;
+    }
   }
 
   static class TestApolloConfigBean2 {
@@ -425,4 +480,25 @@ public class JavaConfigAnnotationTest extends AbstractSpringIntegrationTest {
 
     }
   }
+
+  static class TestApolloConfigChangeListenerWithYamlFile {
+
+    private SettableFuture<ConfigChangeEvent> configChangeEventFuture = SettableFuture.create();
+
+    @ApolloConfig(APPLICATION_YAML_NAMESPACE)
+    private Config yamlConfig;
+
+    @ApolloConfigChangeListener(APPLICATION_YAML_NAMESPACE)
+    private void onChange(ConfigChangeEvent event) {
+      configChangeEventFuture.set(event);
+    }
+
+    public SettableFuture<ConfigChangeEvent> getConfigChangeEventFuture() {
+      return configChangeEventFuture;
+    }
+
+    public Config getYamlConfig() {
+      return yamlConfig;
+    }
+  }
 }

+ 184 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigPlaceholderAutoUpdateTest.java

@@ -7,6 +7,7 @@ import static org.junit.Assert.assertTrue;
 import com.ctrip.framework.apollo.build.MockInjector;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.internals.SimpleConfig;
+import com.ctrip.framework.apollo.internals.YamlConfigFile;
 import com.ctrip.framework.apollo.spring.JavaConfigPlaceholderTest.JsonBean;
 import com.ctrip.framework.apollo.spring.XmlConfigPlaceholderTest.TestXmlBean;
 import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
@@ -71,6 +72,31 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(newBatch, bean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithOneYamlFile() throws Exception {
+    int initialTimeout = 1000;
+    int initialBatch = 2000;
+    int newTimeout = 1001;
+    int newBatch = 2001;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yaml",
+        readYamlContentAsConfigFileProperties("case1.yaml"));
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig12.class);
+
+    TestJavaConfigBean bean = context.getBean(TestJavaConfigBean.class);
+
+    assertEquals(initialTimeout, bean.getTimeout());
+    assertEquals(initialBatch, bean.getBatch());
+
+    configFile.onRepositoryChange("application.yaml", readYamlContentAsConfigFileProperties("case1-new.yaml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(newTimeout, bean.getTimeout());
+    assertEquals(newBatch, bean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateWithValueAndXmlProperty() throws Exception {
     int initialTimeout = 1000;
@@ -106,6 +132,36 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(newBatch, xmlBean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithYamlFileWithValueAndXmlProperty() throws Exception {
+    int initialTimeout = 1000;
+    int initialBatch = 2000;
+    int newTimeout = 1001;
+    int newBatch = 2001;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yaml",
+        readYamlContentAsConfigFileProperties("case1.yaml"));
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig13.class);
+
+    TestJavaConfigBean javaConfigBean = context.getBean(TestJavaConfigBean.class);
+    TestXmlBean xmlBean = context.getBean(TestXmlBean.class);
+
+    assertEquals(initialTimeout, javaConfigBean.getTimeout());
+    assertEquals(initialBatch, javaConfigBean.getBatch());
+    assertEquals(initialTimeout, xmlBean.getTimeout());
+    assertEquals(initialBatch, xmlBean.getBatch());
+
+    configFile.onRepositoryChange("application.yaml", readYamlContentAsConfigFileProperties("case1-new.yaml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(newTimeout, javaConfigBean.getTimeout());
+    assertEquals(newBatch, javaConfigBean.getBatch());
+    assertEquals(newTimeout, xmlBean.getTimeout());
+    assertEquals(newBatch, xmlBean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateDisabled() throws Exception {
     int initialTimeout = 1000;
@@ -213,6 +269,35 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(someBatch, bean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithMultipleNamespacesWithSamePropertiesWithYamlFile() throws Exception {
+    int someTimeout = 1000;
+    int someBatch = 2000;
+    int anotherBatch = 3000;
+    int someNewBatch = 2001;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yml",
+        readYamlContentAsConfigFileProperties("case2.yml"));
+    Properties fxApolloProperties =
+        assembleProperties(TIMEOUT_PROPERTY, String.valueOf(someTimeout), BATCH_PROPERTY, String.valueOf(anotherBatch));
+
+    prepareConfig(FX_APOLLO_NAMESPACE, fxApolloProperties);
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig14.class);
+
+    TestJavaConfigBean bean = context.getBean(TestJavaConfigBean.class);
+
+    assertEquals(someTimeout, bean.getTimeout());
+    assertEquals(someBatch, bean.getBatch());
+
+    configFile.onRepositoryChange("application.yml", readYamlContentAsConfigFileProperties("case2-new.yml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(someTimeout, bean.getTimeout());
+    assertEquals(someNewBatch, bean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateWithNewProperties() throws Exception {
     int initialTimeout = 1000;
@@ -241,6 +326,30 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(newBatch, bean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithNewPropertiesWithYamlFile() throws Exception {
+    int initialTimeout = 1000;
+    int newTimeout = 1001;
+    int newBatch = 2001;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yaml",
+        readYamlContentAsConfigFileProperties("case3.yaml"));
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig12.class);
+
+    TestJavaConfigBean bean = context.getBean(TestJavaConfigBean.class);
+
+    assertEquals(initialTimeout, bean.getTimeout());
+    assertEquals(DEFAULT_BATCH, bean.getBatch());
+
+    configFile.onRepositoryChange("application.yaml", readYamlContentAsConfigFileProperties("case3-new.yaml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(newTimeout, bean.getTimeout());
+    assertEquals(newBatch, bean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateWithIrrelevantProperties() throws Exception {
     int initialTimeout = 1000;
@@ -301,6 +410,29 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(DEFAULT_BATCH, bean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithDeletedPropertiesWithYamlFile() throws Exception {
+    int initialTimeout = 1000;
+    int initialBatch = 2000;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yaml",
+        readYamlContentAsConfigFileProperties("case4.yaml"));
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig12.class);
+
+    TestJavaConfigBean bean = context.getBean(TestJavaConfigBean.class);
+
+    assertEquals(initialTimeout, bean.getTimeout());
+    assertEquals(initialBatch, bean.getBatch());
+
+    configFile.onRepositoryChange("application.yaml", readYamlContentAsConfigFileProperties("case4-new.yaml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(DEFAULT_TIMEOUT, bean.getTimeout());
+    assertEquals(DEFAULT_BATCH, bean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateWithMultipleNamespacesWithSamePropertiesDeleted() throws Exception {
     int someTimeout = 1000;
@@ -389,6 +521,30 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     assertEquals(initialBatch, bean.getBatch());
   }
 
+  @Test
+  public void testAutoUpdateWithTypeMismatchWithYamlFile() throws Exception {
+    int initialTimeout = 1000;
+    int initialBatch = 2000;
+    int newTimeout = 1001;
+
+    YamlConfigFile configFile = prepareYamlConfigFile("application.yaml",
+        readYamlContentAsConfigFileProperties("case5.yaml"));
+
+    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(AppConfig12.class);
+
+    TestJavaConfigBean bean = context.getBean(TestJavaConfigBean.class);
+
+    assertEquals(initialTimeout, bean.getTimeout());
+    assertEquals(initialBatch, bean.getBatch());
+
+    configFile.onRepositoryChange("application.yaml", readYamlContentAsConfigFileProperties("case5-new.yaml"));
+
+    TimeUnit.MILLISECONDS.sleep(100);
+
+    assertEquals(newTimeout, bean.getTimeout());
+    assertEquals(initialBatch, bean.getBatch());
+  }
+
   @Test
   public void testAutoUpdateWithValueInjectedAsParameter() throws Exception {
     int initialTimeout = 1000;
@@ -949,6 +1105,34 @@ public class JavaConfigPlaceholderAutoUpdateTest extends AbstractSpringIntegrati
     }
   }
 
+  @Configuration
+  @EnableApolloConfig("application.yaMl")
+  static class AppConfig12 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
+  @Configuration
+  @EnableApolloConfig("application.yaml")
+  @ImportResource("spring/XmlConfigPlaceholderTest11.xml")
+  static class AppConfig13 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
+  @Configuration
+  @EnableApolloConfig({"application.yml", "FX.apollo"})
+  static class AppConfig14 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
   static class TestJavaConfigBean {
 
     @Value("${timeout:100}")

+ 90 - 7
apollo-client/src/test/java/com/ctrip/framework/apollo/spring/JavaConfigPlaceholderTest.java

@@ -7,10 +7,12 @@ import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
 
 import com.ctrip.framework.apollo.Config;
+import com.ctrip.framework.apollo.PropertiesCompatibleConfigFile;
 import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.spring.annotation.ApolloJsonValue;
 import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
 import java.util.List;
+import java.util.Properties;
 import org.junit.Test;
 import org.springframework.beans.factory.BeanCreationException;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -70,6 +72,38 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
     check(someTimeout, someBatch, AppConfig2.class);
   }
 
+  @Test
+  public void testPropertiesCompatiblePropertySource() throws Exception {
+    int someTimeout = 1000;
+    int someBatch = 2000;
+    Properties properties = mock(Properties.class);
+
+    when(properties.getProperty(TIMEOUT_PROPERTY)).thenReturn(String.valueOf(someTimeout));
+    when(properties.getProperty(BATCH_PROPERTY)).thenReturn(String.valueOf(someBatch));
+    PropertiesCompatibleConfigFile configFile = mock(PropertiesCompatibleConfigFile.class);
+    when(configFile.asProperties()).thenReturn(properties);
+
+    mockConfigFile("application.yaml", configFile);
+
+    check(someTimeout, someBatch, AppConfig9.class);
+  }
+
+  @Test
+  public void testPropertiesCompatiblePropertySourceWithNonNormalizedCase() throws Exception {
+    int someTimeout = 1000;
+    int someBatch = 2000;
+    Properties properties = mock(Properties.class);
+
+    when(properties.getProperty(TIMEOUT_PROPERTY)).thenReturn(String.valueOf(someTimeout));
+    when(properties.getProperty(BATCH_PROPERTY)).thenReturn(String.valueOf(someBatch));
+    PropertiesCompatibleConfigFile configFile = mock(PropertiesCompatibleConfigFile.class);
+    when(configFile.asProperties()).thenReturn(properties);
+
+    mockConfigFile("application.yaml", configFile);
+
+    check(someTimeout, someBatch, AppConfig10.class);
+  }
+
   @Test
   public void testMultiplePropertySources() throws Exception {
     int someTimeout = 1000;
@@ -87,24 +121,27 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
   }
 
   @Test
-  public void testMultiplePropertySourcesWithSameProperties() throws Exception {
+  public void testMultiplePropertiesCompatiblePropertySourcesWithSameProperties() throws Exception {
     int someTimeout = 1000;
     int anotherTimeout = someTimeout + 1;
     int someBatch = 2000;
 
-    Config application = mock(Config.class);
-    when(application.getProperty(eq(TIMEOUT_PROPERTY), anyString())).thenReturn(String.valueOf(someTimeout));
-    when(application.getProperty(eq(BATCH_PROPERTY), anyString())).thenReturn(String.valueOf(someBatch));
-    mockConfig(ConfigConsts.NAMESPACE_APPLICATION, application);
+    Properties properties = mock(Properties.class);
+
+    when(properties.getProperty(TIMEOUT_PROPERTY)).thenReturn(String.valueOf(someTimeout));
+    when(properties.getProperty(BATCH_PROPERTY)).thenReturn(String.valueOf(someBatch));
+    PropertiesCompatibleConfigFile configFile = mock(PropertiesCompatibleConfigFile.class);
+    when(configFile.asProperties()).thenReturn(properties);
+
+    mockConfigFile("application.yml", configFile);
 
     Config fxApollo = mock(Config.class);
     when(fxApollo.getProperty(eq(TIMEOUT_PROPERTY), anyString())).thenReturn(String.valueOf(anotherTimeout));
     mockConfig(FX_APOLLO_NAMESPACE, fxApollo);
 
-    check(someTimeout, someBatch, AppConfig3.class);
+    check(someTimeout, someBatch, AppConfig11.class);
   }
 
-
   @Test
   public void testMultiplePropertySourcesCoverWithSameProperties() throws Exception {
     //Multimap does not maintain the strict input order of namespace.
@@ -124,6 +161,25 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
     check(someTimeout, someBatch, AppConfig6.class);
   }
 
+  @Test
+  public void testMultiplePropertySourcesCoverWithSamePropertiesWithPropertiesCompatiblePropertySource() throws Exception {
+    //Multimap does not maintain the strict input order of namespace.
+    int someTimeout = 1000;
+    int anotherTimeout = someTimeout + 1;
+    int someBatch = 2000;
+
+    Config fxApollo = mock(Config.class);
+    when(fxApollo.getProperty(eq(TIMEOUT_PROPERTY), anyString())).thenReturn(String.valueOf(someTimeout));
+    when(fxApollo.getProperty(eq(BATCH_PROPERTY), anyString())).thenReturn(String.valueOf(someBatch));
+    mockConfig(FX_APOLLO_NAMESPACE, fxApollo);
+
+    Config application = mock(Config.class);
+    when(application.getProperty(eq(TIMEOUT_PROPERTY), anyString())).thenReturn(String.valueOf(anotherTimeout));
+    mockConfig(ConfigConsts.NAMESPACE_APPLICATION, application);
+
+    check(someTimeout, someBatch, AppConfig6.class);
+  }
+
   @Test
   public void testMultiplePropertySourcesWithSamePropertiesWithWeight() throws Exception {
     int someTimeout = 1000;
@@ -424,6 +480,33 @@ public class JavaConfigPlaceholderTest extends AbstractSpringIntegrationTest {
     }
   }
 
+  @Configuration
+  @EnableApolloConfig("application.yaml")
+  static class AppConfig9 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
+  @Configuration
+  @EnableApolloConfig("application.yaMl")
+  static class AppConfig10 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
+  @Configuration
+  @EnableApolloConfig({"application.yml", "FX.apollo"})
+  static class AppConfig11 {
+    @Bean
+    TestJavaConfigBean testJavaConfigBean() {
+      return new TestJavaConfigBean();
+    }
+  }
+
   @Component
   static class TestJavaConfigBean {
     @Value("${timeout:100}")

+ 81 - 0
apollo-client/src/test/java/com/ctrip/framework/apollo/util/yaml/YamlParserTest.java

@@ -0,0 +1,81 @@
+package com.ctrip.framework.apollo.util.yaml;
+
+import static org.junit.Assert.assertTrue;
+
+import java.io.File;
+import java.util.Properties;
+
+import org.junit.Test;
+import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
+import org.springframework.core.io.ByteArrayResource;
+import org.yaml.snakeyaml.parser.ParserException;
+
+import com.google.common.base.Charsets;
+import com.google.common.io.Files;
+
+public class YamlParserTest {
+
+  private YamlParser parser = new YamlParser();
+
+  @Test
+  public void testValidCases() throws Exception {
+    test("case1.yaml");
+    test("case3.yaml");
+    test("case4.yaml");
+    test("case5.yaml");
+    test("case6.yaml");
+    test("case7.yaml");
+  }
+
+  @Test(expected = ParserException.class)
+  public void testcase2() throws Exception {
+    testInvalid("case2.yaml");
+  }
+
+  @Test(expected = ParserException.class)
+  public void testcase8() throws Exception {
+    testInvalid("case8.yaml");
+  }
+
+  private void test(String caseName) throws Exception {
+    File file = new File("src/test/resources/yaml/" + caseName);
+
+    String yamlContent = Files.toString(file, Charsets.UTF_8);
+
+    check(yamlContent);
+  }
+
+  private void testInvalid(String caseName) throws Exception {
+    File file = new File("src/test/resources/yaml/" + caseName);
+
+    String yamlContent = Files.toString(file, Charsets.UTF_8);
+
+    parser.yamlToProperties(yamlContent);
+  }
+
+  private void check(String yamlContent) {
+    YamlPropertiesFactoryBean yamlPropertiesFactoryBean = new YamlPropertiesFactoryBean();
+    yamlPropertiesFactoryBean.setResources(new ByteArrayResource(yamlContent.getBytes()));
+    Properties expected = yamlPropertiesFactoryBean.getObject();
+
+    Properties actual = parser.yamlToProperties(yamlContent);
+
+    assertTrue("expected: " + expected + " actual: " + actual, checkPropertiesEquals(expected, actual));
+  }
+
+  private boolean checkPropertiesEquals(Properties expected, Properties actual) {
+    if (expected == actual)
+      return true;
+
+    if (expected.size() != actual.size())
+      return false;
+
+    for (Object key : expected.keySet()) {
+      if (!expected.getProperty((String) key).equals(actual.getProperty((String) key))) {
+        return false;
+      }
+    }
+
+    return true;
+  }
+}

+ 12 - 0
apollo-client/src/test/resources/spring/XmlConfigPlaceholderTest11.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns:apollo="http://www.ctrip.com/schema/apollo"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+       http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
+
+    <bean class="com.ctrip.framework.apollo.spring.XmlConfigPlaceholderTest.TestXmlBean">
+        <property name="timeout" value="${timeout:100}"/>
+        <property name="batch" value="${batch:200}"/>
+    </bean>
+</beans>

+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case1-new.yaml

@@ -0,0 +1,2 @@
+timeout: 1001
+batch: 2001

+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case1.yaml

@@ -0,0 +1,2 @@
+timeout: 1000
+batch: 2000

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case2-new.yml

@@ -0,0 +1 @@
+batch: 2001

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case2.yml

@@ -0,0 +1 @@
+batch: 2000

+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case3-new.yaml

@@ -0,0 +1,2 @@
+timeout: 1001
+batch: 2001

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case3.yaml

@@ -0,0 +1 @@
+timeout: 1000

+ 0 - 0
apollo-client/src/test/resources/spring/yaml/case4-new.yaml


+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case4.yaml

@@ -0,0 +1,2 @@
+timeout: 1000
+batch: 2000

+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case5-new.yaml

@@ -0,0 +1,2 @@
+timeout: 1001
+batch: newBatch

+ 2 - 0
apollo-client/src/test/resources/spring/yaml/case5.yaml

@@ -0,0 +1,2 @@
+timeout: 1000
+batch: 2000

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case6.yml

@@ -0,0 +1 @@
+apollo.test.testBean: true

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case7.yml

@@ -0,0 +1 @@
+apollo.test.testBean:false

+ 0 - 0
apollo-client/src/test/resources/spring/yaml/case8.yml


+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case9-new.yml

@@ -0,0 +1 @@
+someKey: anotherValue

+ 1 - 0
apollo-client/src/test/resources/spring/yaml/case9.yml

@@ -0,0 +1 @@
+someKey: someValue

+ 25 - 0
apollo-client/src/test/resources/yaml/case1.yaml

@@ -0,0 +1,25 @@
+root:
+  key1: "someValue"
+  key2: 100
+  key3:
+    key4:
+      key5: '(%sender%) %message%'
+      key6: '* %sender% %message%'
+#  commented: "xxx"
+  list:
+  - 'item 1'
+  - 'item 2'
+  intList:
+    - 100
+    - 200
+  listOfMap:
+  - key: '#mychannel'
+    value: ''
+  - key: '#myprivatechannel'
+    value: 'mypassword'
+  listOfList:
+  - - 'a1'
+    - 'a2'
+  - - 'b1'
+    - 'b2'
+  listOfList2: [ ['a1', 'a2'], ['b1', 'b2'] ]

+ 4 - 0
apollo-client/src/test/resources/yaml/case2.yaml

@@ -0,0 +1,4 @@
+root:
+  key1: "someValue"
+  key2: 100
+  key1: "anotherValue"

+ 3 - 0
apollo-client/src/test/resources/yaml/case3.yaml

@@ -0,0 +1,3 @@
+root:
+key1: "someValue"
+key2: 100

+ 148 - 0
apollo-client/src/test/resources/yaml/case4.yaml

@@ -0,0 +1,148 @@
+---  # document start
+
+# Comments in YAML look like this.
+
+################
+# SCALAR TYPES #
+################
+
+# Our root object (which continues for the entire document) will be a map,
+# which is equivalent to a dictionary, hash or object in other languages.
+key: value
+another_key: Another value goes here.
+a_number_value: 100
+scientific_notation: 1e+12
+# The number 1 will be interpreted as a number, not a boolean. if you want
+# it to be interpreted as a boolean, use true
+boolean: true
+null_value: null
+key with spaces: value
+# Notice that strings don't need to be quoted. However, they can be.
+however: 'A string, enclosed in quotes.'
+'Keys can be quoted too.': "Useful if you want to put a ':' in your key."
+single quotes: 'have ''one'' escape pattern'
+double quotes: "have many: \", \0, \t, \u263A, \x0d\x0a == \r\n, and more."
+
+# Multiple-line strings can be written either as a 'literal block' (using |),
+# or a 'folded block' (using '>').
+literal_block: |
+    This entire block of text will be the value of the 'literal_block' key,
+    with line breaks being preserved.
+
+    The literal continues until de-dented, and the leading indentation is
+    stripped.
+
+        Any lines that are 'more-indented' keep the rest of their indentation -
+        these lines will be indented by 4 spaces.
+folded_style: >
+    This entire block of text will be the value of 'folded_style', but this
+    time, all newlines will be replaced with a single space.
+
+    Blank lines, like above, are converted to a newline character.
+
+        'More-indented' lines keep their newlines, too -
+        this text will appear over two lines.
+
+####################
+# COLLECTION TYPES #
+####################
+
+# Nesting uses indentation. 2 space indent is preferred (but not required).
+a_nested_map:
+  key: value
+  another_key: Another Value
+  another_nested_map:
+    hello: hello
+
+# Maps don't have to have string keys.
+0.25: a float key
+
+# Keys can also be complex, like multi-line objects
+# We use ? followed by a space to indicate the start of a complex key.
+? |
+  This is a key
+  that has multiple lines
+: and this is its value
+
+# YAML also allows mapping between sequences with the complex key syntax
+# Some language parsers might complain
+# An example
+? - Manchester United
+  - Real Madrid
+: [2001-01-01, 2002-02-02]
+
+# Sequences (equivalent to lists or arrays) look like this
+# (note that the '-' counts as indentation):
+a_sequence:
+  - Item 1
+  - Item 2
+  - 0.5  # sequences can contain disparate types.
+  - Item 4
+  - key: value
+    another_key: another_value
+  -
+    - This is a sequence
+    - inside another sequence
+  - - - Nested sequence indicators
+      - can be collapsed
+
+# Since YAML is a superset of JSON, you can also write JSON-style maps and
+# sequences:
+json_map: {"key": "value"}
+json_seq: [3, 2, 1, "takeoff"]
+and quotes are optional: {key: [3, 2, 1, takeoff]}
+
+#######################
+# EXTRA YAML FEATURES #
+#######################
+
+# YAML also has a handy feature called 'anchors', which let you easily duplicate
+# content across your document. Both of these keys will have the same value:
+anchored_content: &anchor_name This string will appear as the value of two keys.
+other_anchor: *anchor_name
+
+# Anchors can be used to duplicate/inherit properties
+base: &base
+  name: Everyone has same name
+
+# The regexp << is called Merge Key Language-Independent Type. It is used to
+# indicate that all the keys of one or more specified maps should be inserted
+# into the current map.
+
+foo: &foo
+  <<: *base
+  age: 10
+
+bar: &bar
+  <<: *base
+  age: 20
+
+# foo and bar would also have name: Everyone has same name
+
+# YAML also has tags, which you can use to explicitly declare types.
+explicit_string: !!str 0.5
+
+####################
+# EXTRA YAML TYPES #
+####################
+
+# Strings and numbers aren't the only scalars that YAML can understand.
+# ISO-formatted date and datetime literals are also parsed.
+datetime: 2001-12-15T02:59:43.1Z
+datetime_with_spaces: 2001-12-14 21:59:43.10 -5
+date: 2002-12-14
+
+# YAML also has a set type, which looks like this:
+set:
+  ? item1
+  ? item2
+  ? item3
+or: {item1, item2, item3}
+
+# Sets are just maps with null values; the above is equivalent to:
+set2:
+  item1: null
+  item2: null
+  item3: null
+
+...  # document end

+ 42 - 0
apollo-client/src/test/resources/yaml/case5.yaml

@@ -0,0 +1,42 @@
+---
+- Ada
+- APL
+- ASP
+
+- Assembly
+- Awk
+---
+- Basic
+---
+- C
+- C#    # Note that comments are denoted with ' #' (space and #).
+- C++
+- Cold Fusion
+
+-
+  - HTML
+  - LaTeX
+  - SGML
+  - VRML
+  - XML
+  - YAML
+-
+  - BSD
+  - GNU Hurd
+  - Linux
+
+- 1.1
+- - 2.1
+  - 2.2
+- - - 3.1
+    - 3.2
+    - 3.3
+
+- name: PyYAML
+  status: 4
+  license: MIT
+  language: Python
+- name: PySyck
+  status: 5
+  license: BSD
+  language: Python

+ 36 - 0
apollo-client/src/test/resources/yaml/case6.yaml

@@ -0,0 +1,36 @@
+left hand:
+- Ring of Teleportation
+- Ring of Speed
+
+right hand:
+- Ring of Resist Fire
+- Ring of Resist Cold
+- Ring of Resist Poison
+base armor class: 0
+base damage: [4,4]
+plus to-hit: 12
+plus to-dam: 16
+plus to-ac: 0
+hero:
+  hp: 34
+  sp: 8
+  level: 4
+orc:
+  hp: 12
+  sp: 0
+  level: 2
+
+plain: Scroll of Remove Curse
+single-quoted: 'EASY_KNOW'
+double-quoted: "?"
+literal: |    # Borrowed from http://www.kersbergen.com/flump/religion.html
+  by hjw              ___
+     __              /.-.\
+    /  )_____________\\  Y
+   /_ /=== == === === =\ _\_
+  ( /)=== == === === == Y   \
+   `-------------------(  o  )
+                        \___/
+folded: >
+  It removes all ordinary curses from all equipped items.
+  Heavy or permanent curses are unaffected.

+ 1 - 0
apollo-client/src/test/resources/yaml/case7.yaml

@@ -0,0 +1 @@
+xxx

+ 1 - 0
apollo-client/src/test/resources/yaml/case8.yaml

@@ -0,0 +1 @@
+,

+ 4 - 0
apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/ConfigFileFormat.java

@@ -45,4 +45,8 @@ public enum ConfigFileFormat {
       return false;
     }
   }
+
+  public static boolean isPropertiesCompatible(ConfigFileFormat format) {
+    return format == YAML || format == YML;
+  }
 }

+ 38 - 11
apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/api/ApolloConfigDemo.java

@@ -1,5 +1,6 @@
 package com.ctrip.framework.apollo.demo.api;
 
+import com.ctrip.framework.apollo.internals.YamlConfigFile;
 import com.google.common.base.Charsets;
 
 import com.ctrip.framework.apollo.Config;
@@ -27,9 +28,11 @@ public class ApolloConfigDemo {
   private static final Logger logger = LoggerFactory.getLogger(ApolloConfigDemo.class);
   private String DEFAULT_VALUE = "undefined";
   private Config config;
+  private Config yamlConfig;
   private Config publicConfig;
   private ConfigFile applicationConfigFile;
   private ConfigFile xmlConfigFile;
+  private YamlConfigFile yamlConfigFile;
 
   public ApolloConfigDemo() {
     ConfigChangeListener changeListener = new ConfigChangeListener() {
@@ -46,9 +49,12 @@ public class ApolloConfigDemo {
     };
     config = ConfigService.getAppConfig();
     config.addChangeListener(changeListener);
+    yamlConfig = ConfigService.getConfig("application.yaml");
+    yamlConfig.addChangeListener(changeListener);
     publicConfig = ConfigService.getConfig("TEST1.apollo");
     publicConfig.addChangeListener(changeListener);
     applicationConfigFile = ConfigService.getConfigFile("application", ConfigFileFormat.Properties);
+    // datasources.xml
     xmlConfigFile = ConfigService.getConfigFile("datasources", ConfigFileFormat.XML);
     xmlConfigFile.addChangeListener(new ConfigFileChangeListener() {
       @Override
@@ -56,6 +62,8 @@ public class ApolloConfigDemo {
         logger.info(changeEvent.toString());
       }
     });
+    // application.yaml
+    yamlConfigFile = (YamlConfigFile) ConfigService.getConfigFile("application", ConfigFileFormat.YAML);
   }
 
   private String getConfig(String key) {
@@ -63,6 +71,9 @@ public class ApolloConfigDemo {
     if (DEFAULT_VALUE.equals(result)) {
       result = publicConfig.getProperty(key, DEFAULT_VALUE);
     }
+    if (DEFAULT_VALUE.equals(result)) {
+      result = yamlConfig.getProperty(key, DEFAULT_VALUE);
+    }
     logger.info(String.format("Loading key : %s with value: %s", key, result));
     return result;
   }
@@ -75,6 +86,9 @@ public class ApolloConfigDemo {
       case "xml":
         print(xmlConfigFile);
         return;
+      case "yaml":
+        printYaml(yamlConfigFile);
+        return;
     }
   }
 
@@ -87,6 +101,11 @@ public class ApolloConfigDemo {
     System.out.println(configFile.getContent());
   }
 
+  private void printYaml(YamlConfigFile configFile) {
+    System.out.println("=== Properties for " + configFile.getNamespace() + " is as follows: ");
+    System.out.println(configFile.asProperties());
+  }
+
   private void printEnvInfo() {
     String message = String.format("AppId: %s, Env: %s, DC: %s, IP: %s", Foundation.app()
         .getAppId(), Foundation.server().getEnvType(), Foundation.server().getDataCenter(),
@@ -106,18 +125,26 @@ public class ApolloConfigDemo {
         continue;
       }
       input = input.trim();
-      if (input.equalsIgnoreCase("application")) {
-        apolloConfigDemo.print("application");
-        continue;
-      }
-      if (input.equalsIgnoreCase("xml")) {
-        apolloConfigDemo.print("xml");
-        continue;
-      }
-      if (input.equalsIgnoreCase("quit")) {
-        System.exit(0);
+      try {
+        if (input.equalsIgnoreCase("application")) {
+          apolloConfigDemo.print("application");
+          continue;
+        }
+        if (input.equalsIgnoreCase("xml")) {
+          apolloConfigDemo.print("xml");
+          continue;
+        }
+        if (input.equalsIgnoreCase("yaml") || input.equalsIgnoreCase("yml")) {
+          apolloConfigDemo.print("yaml");
+          continue;
+        }
+        if (input.equalsIgnoreCase("quit")) {
+          System.exit(0);
+        }
+        apolloConfigDemo.getConfig(input);
+      } catch (Throwable ex) {
+        logger.error("some error occurred", ex);
       }
-      apolloConfigDemo.getConfig(input);
     }
   }
 }

+ 1 - 1
apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/common/config/AnotherAppConfig.java

@@ -8,6 +8,6 @@ import org.springframework.context.annotation.Configuration;
  * @author Jason Song(song_s@ctrip.com)
  */
 @Configuration
-@EnableApolloConfig(value = "TEST1.apollo", order = 11)
+@EnableApolloConfig(value = {"TEST1.apollo", "application.yaml"}, order = 11)
 public class AnotherAppConfig {
 }

+ 18 - 0
apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/springBootDemo/config/SampleRedisConfig.java

@@ -15,6 +15,8 @@ import javax.annotation.PostConstruct;
 
 /**
  * You may set up data like the following in Apollo:
+ * <br /><br />
+ * Properties Sample: application.properties
  * <pre>
  * redis.cache.enabled = true
  * redis.cache.expireSeconds = 100
@@ -26,6 +28,22 @@ import javax.annotation.PostConstruct;
  * redis.cache.someList[1] = d
  * </pre>
  *
+ * Yaml Sample: application.yaml
+ * <pre>
+ * redis:
+ *   cache:
+ *     enabled: true
+ *     expireSeconds: 100
+ *     clusterNodes: 1,2
+ *     commandTimeout: 50
+ *     someMap:
+ *       key1: a
+ *       key2: b
+ *     someList:
+ *     - c
+ *     - d
+ * </pre>
+ *
  * To make <code>@ConditionalOnProperty</code> work properly, <code>apollo.bootstrap.enabled</code> should be set to true
  * and <code>redis.cache.enabled</code> should also be set to true. Check 'src/main/resources/application.yml' for more information.
  *

+ 2 - 1
apollo-demo/src/main/java/com/ctrip/framework/apollo/demo/spring/springBootDemo/refresh/SpringBootApolloRefreshConfig.java

@@ -1,5 +1,6 @@
 package com.ctrip.framework.apollo.demo.spring.springBootDemo.refresh;
 
+import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.demo.spring.springBootDemo.config.SampleRedisConfig;
 import com.ctrip.framework.apollo.model.ConfigChangeEvent;
 import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
@@ -27,7 +28,7 @@ public class SpringBootApolloRefreshConfig {
     this.refreshScope = refreshScope;
   }
 
-  @ApolloConfigChangeListener
+  @ApolloConfigChangeListener({ConfigConsts.NAMESPACE_APPLICATION, "TEST1.apollo", "application.yaml"})
   public void onChange(ConfigChangeEvent changeEvent) {
     boolean redisCacheKeysChanged = false;
     for (String changedKey : changeEvent.changedKeys()) {

+ 1 - 1
apollo-demo/src/main/resources/application.yml

@@ -2,4 +2,4 @@ apollo:
   bootstrap:
     enabled: true
     # will inject 'application' and 'TEST1.apollo' namespaces in bootstrap phase
-    namespaces: application,TEST1.apollo
+    namespaces: application,TEST1.apollo,application.yaml

+ 1 - 1
apollo-demo/src/main/resources/spring.xml

@@ -7,7 +7,7 @@
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
        http://www.ctrip.com/schema/apollo http://www.ctrip.com/schema/apollo.xsd">
     <apollo:config order="10"/>
-    <apollo:config namespaces="TEST1.apollo" order="11"/>
+    <apollo:config namespaces="TEST1.apollo,application.yaml" order="11"/>
 
     <bean class="com.ctrip.framework.apollo.demo.spring.xmlConfigDemo.bean.XmlBean">
         <property name="timeout" value="${timeout:200}"/>

+ 2 - 1
apollo-portal/src/main/resources/static/namespace.html

@@ -57,7 +57,8 @@
                             <li>
                                 通过创建一个私有的Namespace可以实现分组管理配置
                             </li>
-                            <li>私有Namespace的格式可以是xml、yml、yaml、json. 您可以通过Apollo-client中ConfigFile接口来获取非properties格式Namespace的内容</li>
+                            <li>私有Namespace的格式可以是xml、yml、yaml、json. 您可以通过apollo-client中ConfigFile接口来获取非properties格式Namespace的内容</li>
+                            <li>1.3.0及以上版本的apollo-client针对yaml/yml提供了更好的支持,可以通过ConfigService.getConfig("someNamespace.yaml")直接获取Config对象,也可以通过@EnableApolloConfig("someNamespace.yaml")注入yaml配置到Spring中去</li>
                         </ul>
                     </div>
                     <div class="row text-right" style="padding-right: 20px;">