Repository: ctripcorp/apollo
Pull request #1315 (2018-08-01 15:35:14)

Refactor meta server related logic so that apollo-core is common enough to go maven central repository.
# Purpose

To refactor meta server related logic so that apollo-core is common enough to go maven centrl repository.

# Brief changes

1. Remove apollo-env.properties from apollo-core, so that it's possible
to deploy apollo-client to maven central repository.
2. Add MetaServerProvider spi so that users could customize how they
find apollo meta servers.
3. Refactor env related utils so that Env.UNKNOWN is returned for
invalid env string instead of null.

# Impact

From 1.0.0 version, apollo-core.jar won't assemble `apollo-env.properties` anymore, so that we could freely deploy apollo client jars to maven central repository, which means a lot convenience for users to access apollo client jars.

However, for existing users, they can't upgrade apollo client jars to 1.0.0 version directly.

# How to config meta server address?

Previously, the meta server address is assembled into apollo-core.jar during build time, so that end user could use apollo with zero config (assume the `env` is configured correctly).

With 1.0.0 version, the meta server address could be configured via the following ways, ordered by priority:

1. JVM system property `apollo.meta`
    * -Dapollo.meta=http://config-service-url
    * System.setProperty("apollo.meta", "http://config-service-url");
2. OS environment variable `APOLLO_META`
    * APOLLO_META=http://config-service-url
    * Please note that Linux os environment variable could not contain dot, so we use underscore instead
3. `apollo.meta` property from `server.properties`
4. `apollo.meta` property from `app.properties`
5. JVM system property `${env}_meta`
    * e.g. if current env is `dev`, then users could config `-Ddev_meta=http://config-service-url`
6. `${env.meta}` from `apollo-env.properties`
    * Users could still put `apollo-env.properties` into classpath or spring boot config folder and apollo will read from it
    * apollo-portal will still use this way to configure meta server addresses

# How to customize meta server address locating logic?

With 1.0.0 version, a MetaServerProvider spi is provided, users could inject their own MetaServerProvider to customize the meta server address locating logic.

Since we use the typical [Java Service Loader pattern](https://docs.oracle.com/javase/7/docs/api/java/util/ServiceLoader.html), it's quite easy to do so.

One thing to note is apollo will iterate through all the MetaServerProvider and will stop when any MetaServerProvider gives the meta server address, so users should be careful about what the order value is. The rule is the smaller order takes the higher priority, so the MetaServerProvider with order 0 will be in front of the MetaServerProvider with order 1.

# How can existing users upgrade to 1.0.0?

1. If existing users currently pass `env` and `${env}_meta` at runtime, they could continue to do so with no impact.
2. If existing users currently place `apollo-env.properties` in their classpath, they could continue to do so with no impact.
3. If existing users assemble meta server addresses into `apollo-core.jar`, then there are several solutions:
    * Create a `xx-company-apollo-client` jar with `apollo-env.properties` in classpath and dependens on apollo-client. Then ask the users to depend on `xx-company-apollo-client` jar instead of `apollo-client`. This might be the most tranparent way.
    * Ask the ops to configure `apollo.meta` in `server.properties` or os env `APOLLO_META`
    * Ask the user to configure `apollo.meta` by `-Dapollo.meta=http://config-service-url` or app.properties manually
    * Ask the user to put the `apollo-env.properties` in their projects' classpath


Patch:
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProvider.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProvider.java
new file mode 100644
index 000000000..00cc0d98d
--- /dev/null
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProvider.java
@@ -0,0 +1,58 @@
+package com.ctrip.framework.apollo.internals;
+
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.core.spi.MetaServerProvider;
+import com.ctrip.framework.foundation.Foundation;
+import com.google.common.base.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DefaultMetaServerProvider implements MetaServerProvider {
+
+  public static final int ORDER = 0;
+  private static final Logger logger = LoggerFactory.getLogger(DefaultMetaServerProvider.class);
+
+  private final String metaServerAddress;
+
+  public DefaultMetaServerProvider() {
+    metaServerAddress = initMetaServerAddress();
+  }
+
+  private String initMetaServerAddress() {
+    // 1. Get from System Property
+    String metaAddress = System.getProperty(ConfigConsts.APOLLO_META_KEY);
+    if (Strings.isNullOrEmpty(metaAddress)) {
+      // 2. Get from OS environment variable, which could not contain dot and is normally in UPPER case
+      metaAddress = System.getenv("APOLLO_META");
+    }
+    if (Strings.isNullOrEmpty(metaAddress)) {
+      // 3. Get from server.properties
+      metaAddress = Foundation.server().getProperty(ConfigConsts.APOLLO_META_KEY, null);
+    }
+    if (Strings.isNullOrEmpty(metaAddress)) {
+      // 4. Get from app.properties
+      metaAddress = Foundation.app().getProperty(ConfigConsts.APOLLO_META_KEY, null);
+    }
+
+    if (Strings.isNullOrEmpty(metaAddress)) {
+      logger.warn("Could not find meta server address, because it is not available in neither (1) JVM system property 'apollo.meta', (2) OS env variable 'APOLLO_META' (3) property 'apollo.meta' from server.properties nor (4) property 'apollo.meta' from app.properties");
+    } else {
+      metaAddress = metaAddress.trim();
+      logger.warn("Located meta services from apollo.meta configuration: {}!", metaAddress);
+    }
+
+    return metaAddress;
+  }
+
+  @Override
+  public String getMetaServerAddress(Env targetEnv) {
+    //for default meta server provider, we don't care the actual environment
+    return metaServerAddress;
+  }
+
+  @Override
+  public int getOrder() {
+    return ORDER;
+  }
+}
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java
index 96663f8e2..174c354e2 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/ConfigUtil.java
@@ -10,7 +10,6 @@ import com.ctrip.framework.apollo.core.ConfigConsts;
 import com.ctrip.framework.apollo.core.MetaDomainConsts;
 import com.ctrip.framework.apollo.core.enums.Env;
 import com.ctrip.framework.apollo.core.enums.EnvUtils;
-import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
 import com.ctrip.framework.foundation.Foundation;
 import com.google.common.base.Strings;
 
@@ -98,19 +97,10 @@ public class ConfigUtil {
   /**
    * Get the current environment.
    *
-   * @return the env
-   * @throws ApolloConfigException if env is set
+   * @return the env, UNKNOWN if env is not set or invalid
    */
   public Env getApolloEnv() {
-    Env env = EnvUtils.transformEnv(Foundation.server().getEnvType());
-    if (env == null) {
-      String path = isOSWindows() ? "C:\\opt\\settings\\server.properties" :
-          "/opt/settings/server.properties";
-      String message = String.format("env is not set, please make sure it is set in %s!", path);
-      logger.error(message);
-      throw new ApolloConfigException(message);
-    }
-    return env;
+    return EnvUtils.transformEnv(Foundation.server().getEnvType());
   }
 
   public String getLocalIp() {
@@ -234,8 +224,7 @@ public class ConfigUtil {
 
   public boolean isInLocalMode() {
     try {
-      Env env = getApolloEnv();
-      return env == Env.LOCAL;
+      return Env.LOCAL == getApolloEnv();
     } catch (Throwable ex) {
       //ignore
     }
diff --git a/apollo-client/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider b/apollo-client/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
new file mode 100644
index 000000000..35db9bfc4
--- /dev/null
+++ b/apollo-client/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
@@ -0,0 +1,1 @@
+com.ctrip.framework.apollo.internals.DefaultMetaServerProvider
diff --git a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/TitanSettings.java b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/TitanSettings.java
index d0702cbc3..be429612c 100644
--- a/apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/TitanSettings.java
+++ b/apollo-common/src/main/java/com/ctrip/framework/apollo/common/datasource/TitanSettings.java
@@ -30,9 +30,6 @@ public class TitanSettings {
 
   public String getTitanUrl() {
     Env env = EnvUtils.transformEnv(Foundation.server().getEnvType());
-    if (env == null) {
-      return "";
-    }
     switch (env) {
       case FAT:
       case FWS:
@@ -49,9 +46,6 @@ public class TitanSettings {
 
   public String getTitanDbname() {
     Env env = EnvUtils.transformEnv(Foundation.server().getEnvType());
-    if (env == null) {
-      return "";
-    }
     switch (env) {
       case FAT:
       case FWS:
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/ConfigConsts.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/ConfigConsts.java
index 03b9e9b37..4d139fb08 100644
--- a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/ConfigConsts.java
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/ConfigConsts.java
@@ -5,6 +5,7 @@ public interface ConfigConsts {
   String CLUSTER_NAME_DEFAULT = "default";
   String CLUSTER_NAMESPACE_SEPARATOR = "+";
   String APOLLO_CLUSTER_KEY = "apollo.cluster";
+  String APOLLO_META_KEY = "apollo.meta";
   String CONFIG_FILE_CONTENT_KEY = "content";
   String NO_APPID_PLACEHOLDER = "ApolloNoAppIdPlaceHolder";
   long NOTIFICATION_ID_PLACEHOLDER = -1;
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/MetaDomainConsts.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/MetaDomainConsts.java
index a996b3713..f0c0e80a3 100644
--- a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/MetaDomainConsts.java
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/MetaDomainConsts.java
@@ -1,101 +1,120 @@
 package com.ctrip.framework.apollo.core;
 
 import com.ctrip.framework.apollo.core.enums.Env;
-import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
-import com.ctrip.framework.apollo.core.utils.NetUtil;
-import com.ctrip.framework.apollo.core.utils.ResourceUtils;
-import com.ctrip.framework.apollo.tracer.Tracer;
-import com.ctrip.framework.apollo.tracer.spi.Transaction;
-import com.ctrip.framework.foundation.Foundation;
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
 import java.util.Collections;
-import java.util.HashMap;
+import java.util.Comparator;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
-import java.util.Properties;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.ctrip.framework.apollo.core.spi.MetaServerProvider;
+import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
+import com.ctrip.framework.apollo.core.utils.NetUtil;
+import com.ctrip.framework.apollo.tracer.Tracer;
+import com.ctrip.framework.apollo.tracer.spi.Transaction;
+import com.ctrip.framework.foundation.internals.ServiceBootstrap;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+
 /**
- * The meta domain will load the meta server from System environment first, if not exist, will load from
- * apollo-env.properties. If neither exists, will load the default meta url.
+ * The meta domain will try to load the meta server address from MetaServerProviders, the default ones are:
+ *
+ * <ul>
+ * <li>com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider</li>
+ * </ul>
  *
- * Currently, apollo supports local/dev/fat/uat/lpt/pro environments.
+ * If no provider could provide the meta server url, the default meta url will be used(http://apollo.meta).
+ * <br />
+ *
+ * 3rd party MetaServerProvider could be injected by typical Java Service Loader pattern.
+ *
+ * @see com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider
  */
 public class MetaDomainConsts {
+  public static final String DEFAULT_META_URL = "http://apollo.meta";
 
-  public static final String DEFAULT_META_URL = "http://config.local";
+  // env -> meta server address cache
+  private static final Map<Env, String> metaServerAddressCache = Maps.newConcurrentMap();
+  private static volatile List<MetaServerProvider> metaServerProviders = null;
 
-  private static final long REFRESH_INTERVAL_IN_SECOND = 60;//1 min
+  private static final long REFRESH_INTERVAL_IN_SECOND = 60;// 1 min
   private static final Logger logger = LoggerFactory.getLogger(MetaDomainConsts.class);
-
-  private static final Map<Env, String> domains = new HashMap<>();
-  private static final Map<String, String> metaServerAddressCache = Maps.newConcurrentMap();
+  // comma separated meta server address -> selected single meta server address cache
+  private static final Map<String, String> selectedMetaServerAddressCache = Maps.newConcurrentMap();
   private static final AtomicBoolean periodicRefreshStarted = new AtomicBoolean(false);
-  private static final AtomicBoolean customizedMetaServiceLogged = new AtomicBoolean(false);
 
-  static {
-    initialize();
-  }
-
-  static void initialize() {
-    Properties prop = new Properties();
-    prop = ResourceUtils.readConfigFile("apollo-env.properties", prop);
-    Properties env = System.getProperties();
-    domains.put(Env.LOCAL,
-        env.getProperty("local_meta", prop.getProperty("local.meta", DEFAULT_META_URL)));
-    domains.put(Env.DEV,
-        env.getProperty("dev_meta", prop.getProperty("dev.meta", DEFAULT_META_URL)));
-    domains.put(Env.FAT,
-        env.getProperty("fat_meta", prop.getProperty("fat.meta", DEFAULT_META_URL)));
-    domains.put(Env.UAT,
-        env.getProperty("uat_meta", prop.getProperty("uat.meta", DEFAULT_META_URL)));
-    domains.put(Env.LPT,
-        env.getProperty("lpt_meta", prop.getProperty("lpt.meta", DEFAULT_META_URL)));
-    domains.put(Env.PRO,
-        env.getProperty("pro_meta", prop.getProperty("pro.meta", DEFAULT_META_URL)));
-  }
+  private static final Object LOCK = new Object();
 
   public static String getDomain(Env env) {
-    // 1. Get meta server address from run time configurations
-    String metaAddress = getCustomizedMetaServerAddress();
-    if (Strings.isNullOrEmpty(metaAddress)) {
-      // 2. Get meta server address from environment
-      metaAddress = domains.get(env);
-    }
-    // 3. if there is more than one address, need to select one
-    if (metaAddress != null && metaAddress.contains(",")) {
-      return selectMetaServerAddress(metaAddress);
+    String metaServerAddress = getMetaServerAddress(env);
+    // if there is more than one address, need to select one
+    if (metaServerAddress.contains(",")) {
+      return selectMetaServerAddress(metaServerAddress);
     }
-    // 4. trim if necessary
-    if (metaAddress != null) {
-      metaAddress = metaAddress.trim();
+    return metaServerAddress;
+  }
+
+  private static String getMetaServerAddress(Env env) {
+    if (!metaServerAddressCache.containsKey(env)) {
+      initMetaServerAddress(env);
     }
-    return metaAddress;
+
+    return metaServerAddressCache.get(env);
   }
 
-  private static String getCustomizedMetaServerAddress() {
-    // 1. Get from System Property
-    String metaAddress = System.getProperty("apollo.meta");
-    if (Strings.isNullOrEmpty(metaAddress)) {
-      // 2. Get from OS environment variable
-      metaAddress = System.getenv("APOLLO_META");
+  private static void initMetaServerAddress(Env env) {
+    if (metaServerProviders == null) {
+      synchronized (LOCK) {
+        if (metaServerProviders == null) {
+          metaServerProviders = initMetaServerProviders();
+        }
+      }
     }
-    if (Strings.isNullOrEmpty(metaAddress)) {
-      metaAddress = Foundation.server().getProperty("apollo.meta", null);
+
+    String metaAddress = null;
+
+    for (MetaServerProvider provider : metaServerProviders) {
+      metaAddress = provider.getMetaServerAddress(env);
+      if (!Strings.isNullOrEmpty(metaAddress)) {
+        logger.info("Located meta server address {} for env {} from {}", metaAddress, env,
+            provider.getClass().getName());
+        break;
+      }
     }
 
-    if (!Strings.isNullOrEmpty(metaAddress) && customizedMetaServiceLogged.compareAndSet(false, true)) {
-      logger.warn("Located meta services from apollo.meta configuration: {}, will not use meta services defined in apollo-env.properties!", metaAddress);
+    if (Strings.isNullOrEmpty(metaAddress)) {
+      // Fallback to default meta address
+      metaAddress = DEFAULT_META_URL;
+      logger.warn(
+          "Meta server address fallback to {} for env {}, because it is not available in all MetaServerProviders",
+          metaAddress, env);
     }
 
-    return metaAddress;
+    metaServerAddressCache.put(env, metaAddress.trim());
+  }
+
+  private static List<MetaServerProvider> initMetaServerProviders() {
+    Iterator<MetaServerProvider> metaServerProviderIterator = ServiceBootstrap.loadAll(MetaServerProvider.class);
+
+    List<MetaServerProvider> metaServerProviders = Lists.newArrayList(metaServerProviderIterator);
+
+    Collections.sort(metaServerProviders, new Comparator<MetaServerProvider>() {
+      @Override
+      public int compare(MetaServerProvider o1, MetaServerProvider o2) {
+        // the smaller order has higher priority
+        return Integer.compare(o1.getOrder(), o2.getOrder());
+      }
+    });
+
+    return metaServerProviders;
   }
 
   /**
@@ -104,18 +123,18 @@ public class MetaDomainConsts {
    *
    * <br />
    *
-   * In production environment, we still suggest using one single domain
-   * like http://config.xxx.com(backed by software load balancers like nginx) instead of multiple ip addresses
+   * In production environment, we still suggest using one single domain like http://config.xxx.com(backed by software
+   * load balancers like nginx) instead of multiple ip addresses
    */
   private static String selectMetaServerAddress(String metaServerAddresses) {
-    String metaAddressSelected = metaServerAddressCache.get(metaServerAddresses);
+    String metaAddressSelected = selectedMetaServerAddressCache.get(metaServerAddresses);
     if (metaAddressSelected == null) {
-      //initialize
+      // initialize
       if (periodicRefreshStarted.compareAndSet(false, true)) {
         schedulePeriodicRefresh();
       }
       updateMetaServerAddresses(metaServerAddresses);
-      metaAddressSelected = metaServerAddressCache.get(metaServerAddresses);
+      metaAddressSelected = selectedMetaServerAddressCache.get(metaServerAddresses);
     }
 
     return metaAddressSelected;
@@ -129,7 +148,7 @@ public class MetaDomainConsts {
 
     try {
       List<String> metaServers = Lists.newArrayList(metaServerAddresses.split(","));
-      //random load balancing
+      // random load balancing
       Collections.shuffle(metaServers);
 
       boolean serverAvailable = false;
@@ -137,22 +156,22 @@ public class MetaDomainConsts {
       for (String address : metaServers) {
         address = address.trim();
         if (NetUtil.pingUrl(address)) {
-          //select the first available meta server
-          metaServerAddressCache.put(metaServerAddresses, address);
+          // select the first available meta server
+          selectedMetaServerAddressCache.put(metaServerAddresses, address);
           serverAvailable = true;
           logger.debug("Selected meta server address {} for {}", address, metaServerAddresses);
           break;
         }
       }
 
-      //we need to make sure the map is not empty, e.g. the first update might be failed
-      if (!metaServerAddressCache.containsKey(metaServerAddresses)) {
-        metaServerAddressCache.put(metaServerAddresses, metaServers.get(0).trim());
+      // we need to make sure the map is not empty, e.g. the first update might be failed
+      if (!selectedMetaServerAddressCache.containsKey(metaServerAddresses)) {
+        selectedMetaServerAddressCache.put(metaServerAddresses, metaServers.get(0).trim());
       }
 
       if (!serverAvailable) {
-        logger.warn("Could not find available meta server for configured meta server addresses: {}, fallback to: {}", metaServerAddresses,
-            metaServerAddressCache.get(metaServerAddresses));
+        logger.warn("Could not find available meta server for configured meta server addresses: {}, fallback to: {}",
+            metaServerAddresses, selectedMetaServerAddressCache.get(metaServerAddresses));
       }
 
       transaction.setStatus(Transaction.SUCCESS);
@@ -165,21 +184,19 @@ public class MetaDomainConsts {
   }
 
   private static void schedulePeriodicRefresh() {
-    ScheduledExecutorService scheduledExecutorService = Executors
-        .newScheduledThreadPool(1, ApolloThreadFactory.create("MetaServiceLocator", true));
+    ScheduledExecutorService scheduledExecutorService =
+        Executors.newScheduledThreadPool(1, ApolloThreadFactory.create("MetaServiceLocator", true));
 
     scheduledExecutorService.scheduleAtFixedRate(new Runnable() {
       @Override
       public void run() {
         try {
-          for (String metaServerAddresses : metaServerAddressCache.keySet()) {
+          for (String metaServerAddresses : selectedMetaServerAddressCache.keySet()) {
             updateMetaServerAddresses(metaServerAddresses);
           }
         } catch (Throwable ex) {
-          logger.warn(
-              String.format("Refreshing meta server address failed, will retry in %d seconds",
-                  REFRESH_INTERVAL_IN_SECOND), ex
-          );
+          logger.warn(String.format("Refreshing meta server address failed, will retry in %d seconds",
+              REFRESH_INTERVAL_IN_SECOND), ex);
         }
       }
     }, REFRESH_INTERVAL_IN_SECOND, REFRESH_INTERVAL_IN_SECOND, TimeUnit.SECONDS);
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java
index dfa30b8e8..9fcc0b881 100644
--- a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/Env.java
@@ -19,11 +19,11 @@ import com.google.common.base.Preconditions;
  * @author Jason Song(song_s@ctrip.com)
  */
 public enum Env{
-  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS;
+  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;
 
   public static Env fromString(String env) {
     Env environment = EnvUtils.transformEnv(env);
-    Preconditions.checkArgument(environment != null, String.format("Env %s is invalid", env));
+    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
     return environment;
   }
 }
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java
index 3795f4396..de760a289 100644
--- a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/enums/EnvUtils.java
@@ -6,7 +6,7 @@ public final class EnvUtils {
   
   public static Env transformEnv(String envName) {
     if (StringUtils.isBlank(envName)) {
-      return null;
+      return Env.UNKNOWN;
     }
     switch (envName.trim().toUpperCase()) {
       case "LPT":
@@ -26,7 +26,7 @@ public final class EnvUtils {
       case "TOOLS":
         return Env.TOOLS;
       default:
-        return null;
+        return Env.UNKNOWN;
     }
   }
 }
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java
new file mode 100644
index 000000000..4d01af3dc
--- /dev/null
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProvider.java
@@ -0,0 +1,51 @@
+package com.ctrip.framework.apollo.core.internals;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.core.spi.MetaServerProvider;
+import com.ctrip.framework.apollo.core.utils.ResourceUtils;
+
+/**
+ * For legacy meta server configuration use, i.e. apollo-env.properties
+ */
+public class LegacyMetaServerProvider implements MetaServerProvider {
+  // make it as lowest as possible, yet not the lowest
+  public static final int ORDER = MetaServerProvider.LOWEST_PRECEDENCE - 1;
+  private static final Map<Env, String> domains = new HashMap<>();
+
+  public LegacyMetaServerProvider() {
+    initialize();
+  }
+
+  private void initialize() {
+    Properties prop = new Properties();
+    prop = ResourceUtils.readConfigFile("apollo-env.properties", prop);
+    Properties env = System.getProperties();
+    domains.put(Env.LOCAL,
+        env.getProperty("local_meta", prop.getProperty("local.meta")));
+    domains.put(Env.DEV,
+        env.getProperty("dev_meta", prop.getProperty("dev.meta")));
+    domains.put(Env.FAT,
+        env.getProperty("fat_meta", prop.getProperty("fat.meta")));
+    domains.put(Env.UAT,
+        env.getProperty("uat_meta", prop.getProperty("uat.meta")));
+    domains.put(Env.LPT,
+        env.getProperty("lpt_meta", prop.getProperty("lpt.meta")));
+    domains.put(Env.PRO,
+        env.getProperty("pro_meta", prop.getProperty("pro.meta")));
+  }
+
+  @Override
+  public String getMetaServerAddress(Env targetEnv) {
+    String metaServerAddress = domains.get(targetEnv);
+    return metaServerAddress == null ? null : metaServerAddress.trim();
+  }
+
+  @Override
+  public int getOrder() {
+    return ORDER;
+  }
+}
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/MetaServerProvider.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/MetaServerProvider.java
new file mode 100644
index 000000000..31c0521a5
--- /dev/null
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/MetaServerProvider.java
@@ -0,0 +1,13 @@
+package com.ctrip.framework.apollo.core.spi;
+
+import com.ctrip.framework.apollo.core.enums.Env;
+
+public interface MetaServerProvider extends Ordered {
+
+  /**
+   * Provide the Apollo meta server address, could be a domain url or comma separated ip addresses, like http://1.2.3.4:8080,http://2.3.4.5:8080.
+   * <br/>
+   * In production environment, we suggest using one single domain like http://config.xxx.com(backed by software load balancers like nginx) instead of multiple ip addresses
+   */
+  String getMetaServerAddress(Env targetEnv);
+}
diff --git a/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/Ordered.java b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/Ordered.java
new file mode 100644
index 000000000..248e11706
--- /dev/null
+++ b/apollo-core/src/main/java/com/ctrip/framework/apollo/core/spi/Ordered.java
@@ -0,0 +1,37 @@
+package com.ctrip.framework.apollo.core.spi;
+
+/**
+ * {@code Ordered} is an interface that can be implemented by objects that
+ * should be <em>orderable</em>, for example in a {@code Collection}.
+ *
+ * <p>The actual {@link #getOrder() order} can be interpreted as prioritization,
+ * with the first object (with the lowest order value) having the highest
+ * priority.
+ */
+public interface Ordered {
+  /**
+   * Useful constant for the highest precedence value.
+   * @see java.lang.Integer#MIN_VALUE
+   */
+  int HIGHEST_PRECEDENCE = Integer.MIN_VALUE;
+
+  /**
+   * Useful constant for the lowest precedence value.
+   * @see java.lang.Integer#MAX_VALUE
+   */
+  int LOWEST_PRECEDENCE = Integer.MAX_VALUE;
+
+
+  /**
+   * Get the order value of this object.
+   * <p>Higher values are interpreted as lower priority. As a consequence,
+   * the object with the lowest value has the highest priority (somewhat
+   * analogous to Servlet {@code load-on-startup} values).
+   * <p>Same order values will result in arbitrary sort positions for the
+   * affected objects.
+   * @return the order value
+   * @see #HIGHEST_PRECEDENCE
+   * @see #LOWEST_PRECEDENCE
+   */
+  int getOrder();
+}
diff --git a/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java b/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java
index 523956be5..a34a518cf 100644
--- a/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java
+++ b/apollo-core/src/main/java/com/ctrip/framework/foundation/internals/ServiceBootstrap.java
@@ -14,7 +14,7 @@ public class ServiceBootstrap {
     return iterator.next();
   }
 
-  private static <S> Iterator<S> loadAll(Class<S> clazz) {
+  public static <S> Iterator<S> loadAll(Class<S> clazz) {
     ServiceLoader<S> loader = ServiceLoader.load(clazz);
 
     return loader.iterator();
diff --git a/apollo-core/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider b/apollo-core/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
new file mode 100644
index 000000000..43484be95
--- /dev/null
+++ b/apollo-core/src/main/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
@@ -0,0 +1,1 @@
+com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider
diff --git a/apollo-portal/src/main/config/apollo-env.properties b/apollo-portal/src/main/config/apollo-env.properties
deleted file mode 100644
index 0c9448882..000000000
--- a/apollo-portal/src/main/config/apollo-env.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-local.meta=http://localhost:8080
-dev.meta=${dev_meta}
-fat.meta=${fat_meta}
-uat.meta=${uat_meta}
-lpt.meta=${lpt_meta}
-pro.meta=${pro_meta}
diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java
index 84ba65d5d..878ba58a2 100644
--- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java
+++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ConsumerController.java
@@ -2,6 +2,7 @@ package com.ctrip.framework.apollo.portal.controller;
 
 import com.ctrip.framework.apollo.common.dto.NamespaceDTO;
 import com.ctrip.framework.apollo.common.exception.BadRequestException;
+import com.ctrip.framework.apollo.core.enums.Env;
 import com.ctrip.framework.apollo.core.enums.EnvUtils;
 import com.ctrip.framework.apollo.core.utils.StringUtils;
 import com.ctrip.framework.apollo.openapi.entity.Consumer;
@@ -90,7 +91,7 @@ public class ConsumerController {
           if (Strings.isNullOrEmpty(env)) {
             continue;
           }
-          if (null == EnvUtils.transformEnv(env)) {
+          if (Env.UNKNOWN == EnvUtils.transformEnv(env)) {
             throw new BadRequestException(String.format("env: %s is illegal", env));
           }
           envList.add(env);
diff --git a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java
index 73ddce862..6c29b417a 100644
--- a/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java
+++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/PermissionController.java
@@ -98,7 +98,7 @@ public class PermissionController {
   public NamespaceEnvRolesAssignedUsers getNamespaceEnvRoles(@PathVariable String appId, @PathVariable String env, @PathVariable String namespaceName) {
 
     // validate env parameter
-    if (null == EnvUtils.transformEnv(env)) {
+    if (Env.UNKNOWN == EnvUtils.transformEnv(env)) {
       throw new BadRequestException("env is illegal");
     }
 
@@ -130,7 +130,7 @@ public class PermissionController {
     }
 
     // validate env parameter
-    if (null == EnvUtils.transformEnv(env)) {
+    if (Env.UNKNOWN == EnvUtils.transformEnv(env)) {
       throw new BadRequestException("env is illegal");
     }
     Set<String> assignedUser = rolePermissionService.assignRoleToUsers(RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType, env),
@@ -152,7 +152,7 @@ public class PermissionController {
       throw new BadRequestException("role type is illegal");
     }
     // validate env parameter
-    if (null == EnvUtils.transformEnv(env)) {
+    if (Env.UNKNOWN == EnvUtils.transformEnv(env)) {
       throw new BadRequestException("env is illegal");
     }
     rolePermissionService.removeRoleFromUsers(RoleUtils.buildNamespaceRoleName(appId, namespaceName, roleType, env),
diff --git a/apollo-core/src/main/resources/apollo-env.properties b/apollo-portal/src/main/resources/apollo-env.properties
similarity index 100%
rename from apollo-core/src/main/resources/apollo-env.properties
rename to apollo-portal/src/main/resources/apollo-env.properties
diff --git a/pom.xml b/pom.xml
index e26a0612b..2ac845ffb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -378,29 +378,6 @@
 						</execution>
 					</executions>
 				</plugin>
-				<plugin>
-					<groupId>org.apache.maven.plugins</groupId>
-					<artifactId>maven-checkstyle-plugin</artifactId>
-					<version>2.17</version>
-					<dependencies>
-						<dependency>
-							<groupId>com.puppycrawl.tools</groupId>
-							<artifactId>checkstyle</artifactId>
-							<version>6.18</version>
-						</dependency>
-						<dependency>
-							<groupId>com.ctrip.framework.apollo</groupId>
-							<artifactId>apollo-buildtools</artifactId>
-							<version>${project.version}</version>
-						</dependency>
-					</dependencies>
-					<configuration>
-						<configLocation>google_checks.xml</configLocation>
-						<headerLocation>LICENSE-2.0.txt</headerLocation>
-						<failOnViolation>false</failOnViolation>
-						<consoleOutput>true</consoleOutput>
-					</configuration>
-				</plugin>
 				<plugin>
 					<groupId>org.codehaus.mojo</groupId>
 					<artifactId>findbugs-maven-plugin</artifactId>
@@ -462,10 +439,6 @@
 				<groupId>org.apache.maven.plugins</groupId>
 				<artifactId>maven-deploy-plugin</artifactId>
 			</plugin>
-			<plugin>
-				<groupId>org.apache.maven.plugins</groupId>
-				<artifactId>maven-checkstyle-plugin</artifactId>
-			</plugin>
 			<plugin>
 				<groupId>org.codehaus.mojo</groupId>
 				<artifactId>findbugs-maven-plugin</artifactId>
@@ -531,7 +504,6 @@
 				<filtering>true</filtering>
 				<includes>
 					<include>application-github.properties</include>
-					<include>apollo-env.properties</include>
 				</includes>
 			</resource>
 			<resource>
@@ -539,7 +511,6 @@
 				<filtering>false</filtering>
 				<excludes>
 					<exclude>application-github.properties</exclude>
-					<exclude>apollo-env.properties</exclude>
 				</excludes>
 			</resource>
 		</resources>
diff --git a/scripts/build.bat b/scripts/build.bat
index 290eced0d..b28dfcfe7 100644
--- a/scripts/build.bat
+++ b/scripts/build.bat
@@ -37,10 +37,4 @@ call mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github
 
 echo "==== building portal finished ===="
 
-echo "==== starting to build client ===="
-
-call mvn clean install -DskipTests -pl apollo-client -am %META_SERVERS_OPTS%
-
-echo "==== building client finished ===="
-
 pause
diff --git a/scripts/build.sh b/scripts/build.sh
index 9f3e2a99f..cb6a539ba 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -36,10 +36,3 @@ echo "==== starting to build portal ===="
 mvn clean package -DskipTests -pl apollo-portal -am -Dapollo_profile=github,auth -Dspring_datasource_url=$apollo_portal_db_url -Dspring_datasource_username=$apollo_portal_db_username -Dspring_datasource_password=$apollo_portal_db_password $META_SERVERS_OPTS
 
 echo "==== building portal finished ===="
-
-echo "==== starting to build client ===="
-
-mvn clean install -DskipTests -pl apollo-client -am $META_SERVERS_OPTS
-
-echo "==== building client finished ===="
-


Test patch:
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProviderTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProviderTest.java
new file mode 100644
index 000000000..f84075f6d
--- /dev/null
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/DefaultMetaServerProviderTest.java
@@ -0,0 +1,38 @@
+package com.ctrip.framework.apollo.internals;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import com.ctrip.framework.apollo.core.ConfigConsts;
+import com.ctrip.framework.apollo.core.enums.Env;
+import org.junit.After;
+import org.junit.Test;
+
+public class DefaultMetaServerProviderTest {
+
+  @After
+  public void tearDown() throws Exception {
+    System.clearProperty(ConfigConsts.APOLLO_META_KEY);
+  }
+
+  @Test
+  public void testWithSystemProperty() throws Exception {
+    String someMetaAddress = "someMetaAddress";
+    Env someEnv = Env.DEV;
+
+    System.setProperty(ConfigConsts.APOLLO_META_KEY, " " + someMetaAddress + " ");
+
+    DefaultMetaServerProvider defaultMetaServerProvider = new DefaultMetaServerProvider();
+
+    assertEquals(someMetaAddress, defaultMetaServerProvider.getMetaServerAddress(someEnv));
+  }
+
+  @Test
+  public void testWithNoSystemProperty() throws Exception {
+    Env someEnv = Env.DEV;
+
+    DefaultMetaServerProvider defaultMetaServerProvider = new DefaultMetaServerProvider();
+
+    assertNull(defaultMetaServerProvider.getMetaServerAddress(someEnv));
+  }
+}
diff --git a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/MetaDomainTest.java b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/MetaDomainTest.java
index bc9471385..67080420c 100644
--- a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/MetaDomainTest.java
+++ b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/MetaDomainTest.java
@@ -5,6 +5,10 @@ import static org.junit.Assert.assertTrue;
 
 import com.ctrip.framework.apollo.BaseIntegrationTest;
 import com.ctrip.framework.apollo.core.enums.Env;
+import com.ctrip.framework.apollo.core.internals.LegacyMetaServerProvider;
+import com.ctrip.framework.apollo.core.spi.MetaServerProvider;
+import com.google.common.collect.Maps;
+import java.util.Map;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.After;
 import org.junit.Test;
@@ -15,10 +19,7 @@ public class MetaDomainTest extends BaseIntegrationTest {
   @After
   public void tearDown() throws Exception {
     super.tearDown();
-    System.clearProperty("fat_meta");
-    System.clearProperty("uat_meta");
-    System.clearProperty("lpt_meta");
-    System.clearProperty("apollo.meta");
+    MockMetaServerProvider.clear();
   }
 
   @Test
@@ -28,16 +29,6 @@ public class MetaDomainTest extends BaseIntegrationTest {
     assertEquals(MetaDomainConsts.DEFAULT_META_URL, MetaDomainConsts.getDomain(Env.PRO));
   }
 
-  @Test
-  public void testGetMetaDomainWithSystemProperty() throws Exception {
-    String someMeta = "some-meta";
-    Env someEnv = Env.DEV;
-
-    System.setProperty("apollo.meta", someMeta);
-
-    assertEquals(someMeta, MetaDomainConsts.getDomain(someEnv));
-  }
-
   @Test
   public void testGetValidAddress() throws Exception {
     String someResponse = "some response";
@@ -46,10 +37,8 @@ public class MetaDomainTest extends BaseIntegrationTest {
     String validServer = " http://localhost:" + PORT + " ";
     String invalidServer = "http://localhost:" + findFreePort();
 
-    System.setProperty("fat_meta", validServer + "," + invalidServer);
-    System.setProperty("uat_meta", invalidServer + "," + validServer);
-
-    MetaDomainConsts.initialize();
+    MockMetaServerProvider.mock(Env.FAT, validServer + "," + invalidServer);
+    MockMetaServerProvider.mock(Env.UAT, invalidServer + "," + validServer);
 
     assertEquals(validServer.trim(), MetaDomainConsts.getDomain(Env.FAT));
     assertEquals(validServer.trim(), MetaDomainConsts.getDomain(Env.UAT));
@@ -60,12 +49,33 @@ public class MetaDomainTest extends BaseIntegrationTest {
     String invalidServer = "http://localhost:" + findFreePort() + " ";
     String anotherInvalidServer = "http://localhost:" + findFreePort() + " ";
 
-    System.setProperty("lpt_meta", invalidServer + "," + anotherInvalidServer);
-
-    MetaDomainConsts.initialize();
+    MockMetaServerProvider.mock(Env.LPT, invalidServer + "," + anotherInvalidServer);
 
     String metaServer = MetaDomainConsts.getDomain(Env.LPT);
 
     assertTrue(metaServer.equals(invalidServer.trim()) || metaServer.equals(anotherInvalidServer.trim()));
   }
+
+  public static class MockMetaServerProvider implements MetaServerProvider {
+
+    private static Map<Env, String> mockMetaServerAddress = Maps.newHashMap();
+
+    private static void mock(Env env, String metaServerAddress) {
+      mockMetaServerAddress.put(env, metaServerAddress);
+    }
+
+    private static void clear() {
+      mockMetaServerAddress.clear();
+    }
+
+    @Override
+    public String getMetaServerAddress(Env targetEnv) {
+      return mockMetaServerAddress.get(targetEnv);
+    }
+
+    @Override
+    public int getOrder() {
+      return LegacyMetaServerProvider.ORDER - 1;// just in front of LegacyMetaServerProvider
+    }
+  }
 }
diff --git a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/enums/EnvUtilsTest.java b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/enums/EnvUtilsTest.java
new file mode 100644
index 000000000..aeac65753
--- /dev/null
+++ b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/enums/EnvUtilsTest.java
@@ -0,0 +1,28 @@
+package com.ctrip.framework.apollo.core.enums;
+
+import static org.junit.Assert.*;
+
+import org.junit.Test;
+
+public class EnvUtilsTest {
+
+  @Test
+  public void testTransformEnv() throws Exception {
+    assertEquals(Env.DEV, EnvUtils.transformEnv(Env.DEV.name()));
+    assertEquals(Env.FAT, EnvUtils.transformEnv(Env.FAT.name().toLowerCase()));
+    assertEquals(Env.UAT, EnvUtils.transformEnv(" " + Env.UAT.name().toUpperCase() + ""));
+    assertEquals(Env.UNKNOWN, EnvUtils.transformEnv("someInvalidEnv"));
+  }
+
+  @Test
+  public void testFromString() throws Exception {
+    assertEquals(Env.DEV, Env.fromString(Env.DEV.name()));
+    assertEquals(Env.FAT, Env.fromString(Env.FAT.name().toLowerCase()));
+    assertEquals(Env.UAT, Env.fromString(" " + Env.UAT.name().toUpperCase() + ""));
+  }
+
+  @Test(expected = IllegalArgumentException.class)
+  public void testFromInvalidString() throws Exception {
+    Env.fromString("someInvalidEnv");
+  }
+}
diff --git a/apollo-core/src/test/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProviderTest.java b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProviderTest.java
new file mode 100644
index 000000000..4445e6a3d
--- /dev/null
+++ b/apollo-core/src/test/java/com/ctrip/framework/apollo/core/internals/LegacyMetaServerProviderTest.java
@@ -0,0 +1,36 @@
+package com.ctrip.framework.apollo.core.internals;
+
+import static org.junit.Assert.assertEquals;
+
+import com.ctrip.framework.apollo.core.enums.Env;
+import org.junit.After;
+import org.junit.Test;
+
+public class LegacyMetaServerProviderTest {
+
+  @After
+  public void tearDown() throws Exception {
+    System.clearProperty("dev_meta");
+  }
+
+  @Test
+  public void testFromPropertyFile() {
+    LegacyMetaServerProvider legacyMetaServerProvider = new LegacyMetaServerProvider();
+    assertEquals("http://localhost:8080", legacyMetaServerProvider.getMetaServerAddress(Env.LOCAL));
+    assertEquals("http://dev:8080", legacyMetaServerProvider.getMetaServerAddress(Env.DEV));
+    assertEquals(null, legacyMetaServerProvider.getMetaServerAddress(Env.PRO));
+  }
+
+  @Test
+  public void testWithSystemProperty() throws Exception {
+    String someDevMetaAddress = "someMetaAddress";
+    String someFatMetaAddress = "someFatMetaAddress";
+    System.setProperty("dev_meta", someDevMetaAddress);
+    System.setProperty("fat_meta", someFatMetaAddress);
+
+    LegacyMetaServerProvider legacyMetaServerProvider = new LegacyMetaServerProvider();
+
+    assertEquals(someDevMetaAddress, legacyMetaServerProvider.getMetaServerAddress(Env.DEV));
+    assertEquals(someFatMetaAddress, legacyMetaServerProvider.getMetaServerAddress(Env.FAT));
+  }
+}
diff --git a/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java b/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java
index 81313ef25..b3b8ed617 100644
--- a/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java
+++ b/apollo-core/src/test/java/com/ctrip/framework/foundation/internals/ServiceBootstrapTest.java
@@ -1,6 +1,5 @@
 package com.ctrip.framework.foundation.internals;
 
-import com.ctrip.framework.foundation.internals.ServiceBootstrap;
 import org.junit.Test;
 
 import java.util.ServiceConfigurationError;
diff --git a/apollo-core/src/test/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider b/apollo-core/src/test/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
new file mode 100644
index 000000000..bd6d23f18
--- /dev/null
+++ b/apollo-core/src/test/resources/META-INF/services/com.ctrip.framework.apollo.core.spi.MetaServerProvider
@@ -0,0 +1,1 @@
+com.ctrip.framework.apollo.core.MetaDomainTest$MockMetaServerProvider