Repository: ctripcorp/apollo
Pull request #3594 (2021-03-10 05:57:10)

replace http client implementation with interface
make it easier to customize http client

codecov-io: # [Codecov](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=h1) Report
> Merging [#3594](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=desc) (7420982) into [master](https://codecov.io/gh/ctripcorp/apollo/commit/f192c774cac43d98f318aeba8d8330b8e2a63e90?el=desc) (f192c77) will **decrease** coverage by `0.21%`.
> The diff coverage is `77.61%`.

[![Impacted file tree graph](https://codecov.io/gh/ctripcorp/apollo/pull/3594/graphs/tree.svg?width=650&height=150&src=pr&token=2USkCxREbZ)](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##             master    #3594      +/-   ##
============================================
- Coverage     51.00%   50.78%   -0.22%     
  Complexity     2328     2328              
============================================
  Files           448      449       +1     
  Lines         14006    14066      +60     
  Branches       1424     1434      +10     
============================================
  Hits           7144     7144              
- Misses         6373     6433      +60     
  Partials        489      489              
```


| [Impacted Files](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=tree) | Coverage Δ | Complexity Δ | |
|---|---|---|---|
| [...com/ctrip/framework/apollo/util/http/HttpUtil.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL3V0aWwvaHR0cC9IdHRwVXRpbC5qYXZh) | `0.00% <ø> (-75.00%)` | `0.00 <0.00> (-9.00)` | |
| [.../framework/apollo/util/http/DefaultHttpClient.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL3V0aWwvaHR0cC9EZWZhdWx0SHR0cENsaWVudC5qYXZh) | `75.00% <75.00%> (ø)` | `9.00 <9.00> (?)` | |
| [...amework/apollo/internals/ConfigServiceLocator.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL2ludGVybmFscy9Db25maWdTZXJ2aWNlTG9jYXRvci5qYXZh) | `89.58% <100.00%> (ø)` | `20.00 <0.00> (ø)` | |
| [...ip/framework/apollo/internals/DefaultInjector.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL2ludGVybmFscy9EZWZhdWx0SW5qZWN0b3IuamF2YQ==) | `68.00% <100.00%> (ø)` | `3.00 <0.00> (ø)` | |
| [.../apollo/internals/RemoteConfigLongPollService.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL2ludGVybmFscy9SZW1vdGVDb25maWdMb25nUG9sbFNlcnZpY2UuamF2YQ==) | `77.71% <100.00%> (-0.61%)` | `28.00 <0.00> (-1.00)` | |
| [...ework/apollo/internals/RemoteConfigRepository.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNsaWVudC9zcmMvbWFpbi9qYXZhL2NvbS9jdHJpcC9mcmFtZXdvcmsvYXBvbGxvL2ludGVybmFscy9SZW1vdGVDb25maWdSZXBvc2l0b3J5LmphdmE=) | `89.37% <100.00%> (ø)` | `25.00 <0.00> (ø)` | |
| [...ervice/service/ReleaseMessageServiceWithCache.java](https://codecov.io/gh/ctripcorp/apollo/pull/3594/diff?src=pr&el=tree#diff-YXBvbGxvLWNvbmZpZ3NlcnZpY2Uvc3JjL21haW4vamF2YS9jb20vY3RyaXAvZnJhbWV3b3JrL2Fwb2xsby9jb25maWdzZXJ2aWNlL3NlcnZpY2UvUmVsZWFzZU1lc3NhZ2VTZXJ2aWNlV2l0aENhY2hlLmphdmE=) | `85.88% <0.00%> (+1.17%)` | `24.00% <0.00%> (+1.00%)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=footer). Last update [de34fe5...7420982](https://codecov.io/gh/ctripcorp/apollo/pull/3594?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConfigServiceLocator.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConfigServiceLocator.java
index 3f2e1cb74..59d00dce1 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConfigServiceLocator.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/ConfigServiceLocator.java
@@ -22,7 +22,7 @@ import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.ExceptionUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
@@ -33,7 +33,7 @@ import com.google.gson.reflect.TypeToken;
 
 public class ConfigServiceLocator {
   private static final Logger logger = LoggerFactory.getLogger(ConfigServiceLocator.class);
-  private HttpUtil m_httpUtil;
+  private HttpClient m_httpClient;
   private ConfigUtil m_configUtil;
   private AtomicReference<List<ServiceDTO>> m_configServices;
   private Type m_responseType;
@@ -49,7 +49,7 @@ public class ConfigServiceLocator {
     m_configServices = new AtomicReference<>(initial);
     m_responseType = new TypeToken<List<ServiceDTO>>() {
     }.getType();
-    m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
+    m_httpClient = ApolloInjector.getInstance(HttpClient.class);
     m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
     this.m_executorService = Executors.newScheduledThreadPool(1,
         ApolloThreadFactory.create("ConfigServiceLocator", true));
@@ -151,7 +151,7 @@ public class ConfigServiceLocator {
       Transaction transaction = Tracer.newTransaction("Apollo.MetaService", "getConfigService");
       transaction.addData("Url", url);
       try {
-        HttpResponse<List<ServiceDTO>> response = m_httpUtil.doGet(request, m_responseType);
+        HttpResponse<List<ServiceDTO>> response = m_httpClient.doGet(request, m_responseType);
         transaction.setStatus(Transaction.SUCCESS);
         List<ServiceDTO> services = response.getBody();
         if (services == null || services.isEmpty()) {
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java
index be1f9d94c..c1d45e489 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/DefaultInjector.java
@@ -12,7 +12,8 @@ import com.ctrip.framework.apollo.tracer.Tracer;
 import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.factory.DefaultPropertiesFactory;
 import com.ctrip.framework.apollo.util.factory.PropertiesFactory;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.DefaultHttpClient;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 
 import com.ctrip.framework.apollo.util.yaml.YamlParser;
 import com.ctrip.framework.foundation.internals.ServiceBootstrap;
@@ -83,7 +84,7 @@ public class DefaultInjector implements Injector {
       bind(ConfigRegistry.class).to(DefaultConfigRegistry.class).in(Singleton.class);
       bind(ConfigFactory.class).to(DefaultConfigFactory.class).in(Singleton.class);
       bind(ConfigUtil.class).in(Singleton.class);
-      bind(HttpUtil.class).in(Singleton.class);
+      bind(HttpClient.class).to(DefaultHttpClient.class).in(Singleton.class);
       bind(ConfigServiceLocator.class).in(Singleton.class);
       bind(RemoteConfigLongPollService.class).in(Singleton.class);
       bind(YamlParser.class).in(Singleton.class);
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java
index 0ad21b7ec..48601438d 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollService.java
@@ -18,7 +18,7 @@ import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.ExceptionUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.HashMultimap;
@@ -65,7 +65,7 @@ public class RemoteConfigLongPollService {
   private Type m_responseType;
   private static final Gson GSON = new Gson();
   private ConfigUtil m_configUtil;
-  private HttpUtil m_httpUtil;
+  private HttpClient m_httpClient;
   private ConfigServiceLocator m_serviceLocator;
 
   /**
@@ -84,7 +84,7 @@ public class RemoteConfigLongPollService {
     m_responseType = new TypeToken<List<ApolloConfigNotification>>() {
     }.getType();
     m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
-    m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
+    m_httpClient = ApolloInjector.getInstance(HttpClient.class);
     m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
     m_longPollRateLimiter = RateLimiter.create(m_configUtil.getLongPollQPS());
   }
@@ -171,7 +171,7 @@ public class RemoteConfigLongPollService {
         transaction.addData("Url", url);
 
         final HttpResponse<List<ApolloConfigNotification>> response =
-            m_httpUtil.doGet(request, m_responseType);
+            m_httpClient.doGet(request, m_responseType);
 
         logger.debug("Long polling response: {}, url: {}", response.getStatusCode(), url);
         if (response.getStatusCode() == 200 && response.getBody() != null) {
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java
index fb0443fa4..2c4f98a79 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/internals/RemoteConfigRepository.java
@@ -20,7 +20,7 @@ import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.ExceptionUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 import com.google.common.base.Joiner;
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
@@ -52,7 +52,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
   private static final Escaper queryParamEscaper = UrlEscapers.urlFormParameterEscaper();
 
   private final ConfigServiceLocator m_serviceLocator;
-  private final HttpUtil m_httpUtil;
+  private final HttpClient m_httpClient;
   private final ConfigUtil m_configUtil;
   private final RemoteConfigLongPollService remoteConfigLongPollService;
   private volatile AtomicReference<ApolloConfig> m_configCache;
@@ -79,7 +79,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
     m_namespace = namespace;
     m_configCache = new AtomicReference<>();
     m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
-    m_httpUtil = ApolloInjector.getInstance(HttpUtil.class);
+    m_httpClient = ApolloInjector.getInstance(HttpClient.class);
     m_serviceLocator = ApolloInjector.getInstance(ConfigServiceLocator.class);
     remoteConfigLongPollService = ApolloInjector.getInstance(RemoteConfigLongPollService.class);
     m_longPollServiceDto = new AtomicReference<>();
@@ -218,7 +218,7 @@ public class RemoteConfigRepository extends AbstractConfigRepository {
         transaction.addData("Url", url);
         try {
 
-          HttpResponse<ApolloConfig> response = m_httpUtil.doGet(request, ApolloConfig.class);
+          HttpResponse<ApolloConfig> response = m_httpClient.doGet(request, ApolloConfig.class);
           m_configNeedForceRefresh.set(false);
           m_loadConfigFailSchedulePolicy.success();
 
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/DefaultHttpClient.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/DefaultHttpClient.java
new file mode 100644
index 000000000..79c7802b0
--- /dev/null
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/DefaultHttpClient.java
@@ -0,0 +1,169 @@
+package com.ctrip.framework.apollo.util.http;
+
+import com.ctrip.framework.apollo.build.ApolloInjector;
+import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
+import com.ctrip.framework.apollo.exceptions.ApolloConfigStatusCodeException;
+import com.ctrip.framework.apollo.util.ConfigUtil;
+import com.google.common.base.Function;
+import com.google.common.io.CharStreams;
+import com.google.gson.Gson;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.lang.reflect.Type;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+public class DefaultHttpClient implements HttpClient {
+  private ConfigUtil m_configUtil;
+  private static final Gson GSON = new Gson();
+
+  /**
+   * Constructor.
+   */
+  public DefaultHttpClient() {
+    m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
+  }
+
+  /**
+   * Do get operation for the http request.
+   *
+   * @param httpRequest  the request
+   * @param responseType the response type
+   * @return the response
+   * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
+   */
+  @Override
+  public <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Class<T> responseType) {
+    Function<String, T> convertResponse = new Function<String, T>() {
+      @Override
+      public T apply(String input) {
+        return GSON.fromJson(input, responseType);
+      }
+    };
+
+    return doGetWithSerializeFunction(httpRequest, convertResponse);
+  }
+
+  /**
+   * Do get operation for the http request.
+   *
+   * @param httpRequest  the request
+   * @param responseType the response type
+   * @return the response
+   * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
+   */
+  @Override
+  public <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Type responseType) {
+    Function<String, T> convertResponse = new Function<String, T>() {
+      @Override
+      public T apply(String input) {
+        return GSON.fromJson(input, responseType);
+      }
+    };
+
+    return doGetWithSerializeFunction(httpRequest, convertResponse);
+  }
+
+  private <T> HttpResponse<T> doGetWithSerializeFunction(HttpRequest httpRequest,
+                                                         Function<String, T> serializeFunction) {
+    InputStreamReader isr = null;
+    InputStreamReader esr = null;
+    int statusCode;
+    try {
+      HttpURLConnection conn = (HttpURLConnection) new URL(httpRequest.getUrl()).openConnection();
+
+      conn.setRequestMethod("GET");
+
+      Map<String, String> headers = httpRequest.getHeaders();
+      if (headers != null && headers.size() > 0) {
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+          conn.setRequestProperty(entry.getKey(), entry.getValue());
+        }
+      }
+
+      int connectTimeout = httpRequest.getConnectTimeout();
+      if (connectTimeout < 0) {
+        connectTimeout = m_configUtil.getConnectTimeout();
+      }
+
+      int readTimeout = httpRequest.getReadTimeout();
+      if (readTimeout < 0) {
+        readTimeout = m_configUtil.getReadTimeout();
+      }
+
+      conn.setConnectTimeout(connectTimeout);
+      conn.setReadTimeout(readTimeout);
+
+      conn.connect();
+
+      statusCode = conn.getResponseCode();
+      String response;
+
+      try {
+        isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
+        response = CharStreams.toString(isr);
+      } catch (IOException ex) {
+        /**
+         * according to https://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html,
+         * we should clean up the connection by reading the response body so that the connection
+         * could be reused.
+         */
+        InputStream errorStream = conn.getErrorStream();
+
+        if (errorStream != null) {
+          esr = new InputStreamReader(errorStream, StandardCharsets.UTF_8);
+          try {
+            CharStreams.toString(esr);
+          } catch (IOException ioe) {
+            //ignore
+          }
+        }
+
+        // 200 and 304 should not trigger IOException, thus we must throw the original exception out
+        if (statusCode == 200 || statusCode == 304) {
+          throw ex;
+        }
+        // for status codes like 404, IOException is expected when calling conn.getInputStream()
+        throw new ApolloConfigStatusCodeException(statusCode, ex);
+      }
+
+      if (statusCode == 200) {
+        return new HttpResponse<>(statusCode, serializeFunction.apply(response));
+      }
+
+      if (statusCode == 304) {
+        return new HttpResponse<>(statusCode, null);
+      }
+    } catch (ApolloConfigStatusCodeException ex) {
+      throw ex;
+    } catch (Throwable ex) {
+      throw new ApolloConfigException("Could not complete get operation", ex);
+    } finally {
+      if (isr != null) {
+        try {
+          isr.close();
+        } catch (IOException ex) {
+          // ignore
+        }
+      }
+
+      if (esr != null) {
+        try {
+          esr.close();
+        } catch (IOException ex) {
+          // ignore
+        }
+      }
+    }
+
+    throw new ApolloConfigStatusCodeException(statusCode,
+        String.format("Get operation failed for %s", httpRequest.getUrl()));
+  }
+
+}
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpClient.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpClient.java
new file mode 100644
index 000000000..d53bf0308
--- /dev/null
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpClient.java
@@ -0,0 +1,32 @@
+package com.ctrip.framework.apollo.util.http;
+
+import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
+import java.lang.reflect.Type;
+
+/**
+ * @author Jason Song(song_s@ctrip.com)
+ */
+public interface HttpClient {
+
+  /**
+   * Do get operation for the http request.
+   *
+   * @param httpRequest  the request
+   * @param responseType the response type
+   * @return the response
+   * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
+   */
+  <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Class<T> responseType)
+      throws ApolloConfigException;
+
+  /**
+   * Do get operation for the http request.
+   *
+   * @param httpRequest  the request
+   * @param responseType the response type
+   * @return the response
+   * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
+   */
+  <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Type responseType)
+      throws ApolloConfigException;
+}
diff --git a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java
index dbfff20f1..e0d08d72b 100644
--- a/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java
+++ b/apollo-client/src/main/java/com/ctrip/framework/apollo/util/http/HttpUtil.java
@@ -1,33 +1,23 @@
 package com.ctrip.framework.apollo.util.http;
 
-import com.ctrip.framework.apollo.build.ApolloInjector;
 import com.ctrip.framework.apollo.exceptions.ApolloConfigException;
-import com.ctrip.framework.apollo.exceptions.ApolloConfigStatusCodeException;
-import com.ctrip.framework.apollo.util.ConfigUtil;
-import com.google.common.base.Function;
-import com.google.common.io.CharStreams;
-import com.google.gson.Gson;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.lang.reflect.Type;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.nio.charset.StandardCharsets;
-import java.util.Map;
 
 /**
  * @author Jason Song(song_s@ctrip.com)
+ * @deprecated in favor of the interface {@link HttpClient} and it's default implementation {@link
+ * DefaultHttpClient}
  */
-public class HttpUtil {
-  private ConfigUtil m_configUtil;
-  private static final Gson GSON = new Gson();
+@Deprecated
+public class HttpUtil implements HttpClient {
+
+  private HttpClient m_httpClient;
 
   /**
    * Constructor.
    */
   public HttpUtil() {
-    m_configUtil = ApolloInjector.getInstance(ConfigUtil.class);
+    m_httpClient = new DefaultHttpClient();
   }
 
   /**
@@ -38,15 +28,9 @@ public class HttpUtil {
    * @return the response
    * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
    */
+  @Override
   public <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Class<T> responseType) {
-    Function<String, T> convertResponse = new Function<String, T>() {
-      @Override
-      public T apply(String input) {
-        return GSON.fromJson(input, responseType);
-      }
-    };
-
-    return doGetWithSerializeFunction(httpRequest, convertResponse);
+    return m_httpClient.doGet(httpRequest, responseType);
   }
 
   /**
@@ -57,111 +41,8 @@ public class HttpUtil {
    * @return the response
    * @throws ApolloConfigException if any error happened or response code is neither 200 nor 304
    */
+  @Override
   public <T> HttpResponse<T> doGet(HttpRequest httpRequest, final Type responseType) {
-    Function<String, T> convertResponse = new Function<String, T>() {
-      @Override
-      public T apply(String input) {
-        return GSON.fromJson(input, responseType);
-      }
-    };
-
-    return doGetWithSerializeFunction(httpRequest, convertResponse);
-  }
-
-  private <T> HttpResponse<T> doGetWithSerializeFunction(HttpRequest httpRequest,
-                                                         Function<String, T> serializeFunction) {
-    InputStreamReader isr = null;
-    InputStreamReader esr = null;
-    int statusCode;
-    try {
-      HttpURLConnection conn = (HttpURLConnection) new URL(httpRequest.getUrl()).openConnection();
-
-      conn.setRequestMethod("GET");
-
-      Map<String, String> headers = httpRequest.getHeaders();
-      if (headers != null && headers.size() > 0) {
-        for (Map.Entry<String, String> entry : headers.entrySet()) {
-          conn.setRequestProperty(entry.getKey(), entry.getValue());
-        }
-      }
-
-      int connectTimeout = httpRequest.getConnectTimeout();
-      if (connectTimeout < 0) {
-        connectTimeout = m_configUtil.getConnectTimeout();
-      }
-
-      int readTimeout = httpRequest.getReadTimeout();
-      if (readTimeout < 0) {
-        readTimeout = m_configUtil.getReadTimeout();
-      }
-
-      conn.setConnectTimeout(connectTimeout);
-      conn.setReadTimeout(readTimeout);
-
-      conn.connect();
-
-      statusCode = conn.getResponseCode();
-      String response;
-
-      try {
-        isr = new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8);
-        response = CharStreams.toString(isr);
-      } catch (IOException ex) {
-        /**
-         * according to https://docs.oracle.com/javase/7/docs/technotes/guides/net/http-keepalive.html,
-         * we should clean up the connection by reading the response body so that the connection
-         * could be reused.
-         */
-        InputStream errorStream = conn.getErrorStream();
-
-        if (errorStream != null) {
-          esr = new InputStreamReader(errorStream, StandardCharsets.UTF_8);
-          try {
-            CharStreams.toString(esr);
-          } catch (IOException ioe) {
-            //ignore
-          }
-        }
-
-        // 200 and 304 should not trigger IOException, thus we must throw the original exception out
-        if (statusCode == 200 || statusCode == 304) {
-          throw ex;
-        }
-        // for status codes like 404, IOException is expected when calling conn.getInputStream()
-        throw new ApolloConfigStatusCodeException(statusCode, ex);
-      }
-
-      if (statusCode == 200) {
-        return new HttpResponse<>(statusCode, serializeFunction.apply(response));
-      }
-
-      if (statusCode == 304) {
-        return new HttpResponse<>(statusCode, null);
-      }
-    } catch (ApolloConfigStatusCodeException ex) {
-      throw ex;
-    } catch (Throwable ex) {
-      throw new ApolloConfigException("Could not complete get operation", ex);
-    } finally {
-      if (isr != null) {
-        try {
-          isr.close();
-        } catch (IOException ex) {
-          // ignore
-        }
-      }
-
-      if (esr != null) {
-        try {
-          esr.close();
-        } catch (IOException ex) {
-          // ignore
-        }
-      }
-    }
-
-    throw new ApolloConfigStatusCodeException(statusCode,
-        String.format("Get operation failed for %s", httpRequest.getUrl()));
+    return m_httpClient.doGet(httpRequest, responseType);
   }
-
 }


Test patch:
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java
index b8668d2d0..5485d65d3 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigLongPollServiceTest.java
@@ -20,7 +20,7 @@ import com.ctrip.framework.apollo.core.signature.Signature;
 import com.ctrip.framework.apollo.util.ConfigUtil;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.net.HttpHeaders;
@@ -51,7 +51,7 @@ public class RemoteConfigLongPollServiceTest {
   @Mock
   private HttpResponse<List<ApolloConfigNotification>> pollResponse;
   @Mock
-  private HttpUtil httpUtil;
+  private HttpClient httpClient;
   @Mock
   private ConfigServiceLocator configServiceLocator;
   private Type responseType;
@@ -63,7 +63,7 @@ public class RemoteConfigLongPollServiceTest {
 
   @Before
   public void setUp() throws Exception {
-    MockInjector.setInstance(HttpUtil.class, httpUtil);
+    MockInjector.setInstance(HttpClient.class, httpClient);
 
     someServerUrl = "http://someServer";
     ServiceDTO serviceDTO = mock(ServiceDTO.class);
@@ -114,7 +114,7 @@ public class RemoteConfigLongPollServiceTest {
         longPollFinished.set(true);
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     remoteConfigLongPollService.submit(someNamespace, someRepository);
 
@@ -156,7 +156,7 @@ public class RemoteConfigLongPollServiceTest {
 
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     final SettableFuture<Boolean> onNotified = SettableFuture.create();
     doAnswer(new Answer<Void>() {
@@ -218,7 +218,7 @@ public class RemoteConfigLongPollServiceTest {
 
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     final SettableFuture<Boolean> onNotified = SettableFuture.create();
     doAnswer(new Answer<Void>() {
@@ -289,7 +289,7 @@ public class RemoteConfigLongPollServiceTest {
 
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     final SettableFuture<Boolean> onAnotherRepositoryNotified = SettableFuture.create();
     doAnswer(new Answer<Void>() {
@@ -352,7 +352,7 @@ public class RemoteConfigLongPollServiceTest {
 
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     final SettableFuture<Boolean> someRepositoryNotified = SettableFuture.create();
     doAnswer(new Answer<Void>() {
@@ -421,7 +421,7 @@ public class RemoteConfigLongPollServiceTest {
 
         return pollResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), eq(responseType));
+    }).when(httpClient).doGet(any(HttpRequest.class), eq(responseType));
 
     final SettableFuture<Boolean> onNotified = SettableFuture.create();
     doAnswer(new Answer<Void>() {
diff --git a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java
index 49c658da9..2e46cb4c8 100644
--- a/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java
+++ b/apollo-client/src/test/java/com/ctrip/framework/apollo/internals/RemoteConfigRepositoryTest.java
@@ -28,7 +28,7 @@ import com.ctrip.framework.apollo.util.OrderedProperties;
 import com.ctrip.framework.apollo.util.factory.PropertiesFactory;
 import com.ctrip.framework.apollo.util.http.HttpRequest;
 import com.ctrip.framework.apollo.util.http.HttpResponse;
-import com.ctrip.framework.apollo.util.http.HttpUtil;
+import com.ctrip.framework.apollo.util.http.HttpClient;
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
@@ -63,7 +63,7 @@ public class RemoteConfigRepositoryTest {
   private String someNamespace;
   private String someServerUrl;
   private ConfigUtil configUtil;
-  private HttpUtil httpUtil;
+  private HttpClient httpClient;
   @Mock
   private static HttpResponse<ApolloConfig> someResponse;
   @Mock
@@ -93,8 +93,8 @@ public class RemoteConfigRepositoryTest {
     when(configServiceLocator.getConfigServices()).thenReturn(Lists.newArrayList(serviceDTO));
     MockInjector.setInstance(ConfigServiceLocator.class, configServiceLocator);
 
-    httpUtil = spy(new MockHttpUtil());
-    MockInjector.setInstance(HttpUtil.class, httpUtil);
+    httpClient = spy(new MockHttpClient());
+    MockInjector.setInstance(HttpClient.class, httpClient);
 
     remoteConfigLongPollService = new RemoteConfigLongPollService();
 
@@ -191,7 +191,7 @@ public class RemoteConfigRepositoryTest {
 
         return someResponse;
       }
-    }).when(httpUtil).doGet(any(HttpRequest.class), any(Class.class));
+    }).when(httpClient).doGet(any(HttpRequest.class), any(Class.class));
 
     RemoteConfigRepository remoteConfigRepository = new RemoteConfigRepository(someNamespace);
 
@@ -304,7 +304,7 @@ public class RemoteConfigRepositoryTest {
 
     final ArgumentCaptor<HttpRequest> httpRequestArgumentCaptor = ArgumentCaptor
         .forClass(HttpRequest.class);
-    verify(httpUtil, atLeast(2)).doGet(httpRequestArgumentCaptor.capture(), eq(ApolloConfig.class));
+    verify(httpClient, atLeast(2)).doGet(httpRequestArgumentCaptor.capture(), eq(ApolloConfig.class));
 
     HttpRequest request = httpRequestArgumentCaptor.getValue();
 
@@ -407,7 +407,7 @@ public class RemoteConfigRepositoryTest {
     }
   }
 
-  public static class MockHttpUtil extends HttpUtil {
+  public static class MockHttpClient implements HttpClient {
 
     @Override
     public <T> HttpResponse<T> doGet(HttpRequest httpRequest, Class<T> responseType) {