因爲工做上的業務本人常常與第三方系統交互,因此常常會使用HttpClient與第三方進行通訊。對於交易類的接口,訂單狀態是相當重要的。java
這就牽扯到一系列問題:apache
HttpClient是否有默認的重試策略?重試策略原理?如何禁止重試?安全
接下來,本文將從源碼中探討這些問題。源碼下載地址:http://hc.apache.org/downloads.cgi,版本是4.5.5。服務器
通常而言,得到HttpClient實例的方法有兩種:cookie
1.HttpClients.custom().setXXX().build()
2.HttpClients.build()
第一種方法用來定製一些HttpClient的屬性,好比https證書,代理服務器,http過濾器,鏈接池管理器等自定義的用法。app
第二種方法用來得到一個默認的HttpClient實例。less
這兩種方法得到都是CloseableHttpClient實例,且都是經過HttpClientBuilder的build()構建的。異步
能夠看到,上面的兩種用法最終都獲得了一個InternalHttpClient,是抽象類CloseableHttpClient的一種實現。socket
public CloseableHttpClient build() { //省略若干行 return new InternalHttpClient( execChain, connManagerCopy, routePlannerCopy, cookieSpecRegistryCopy, authSchemeRegistryCopy, defaultCookieStore, defaultCredentialsProvider, defaultRequestConfig != null ? defaultRequestConfig : RequestConfig.DEFAULT, closeablesCopy); } }
這裏有不少配置化參數,這裏咱們重點關注一下execChain這個執行鏈。ide
能夠看到執行鏈有多種實現,好比
這麼多執行器,是怎麼用到了重試執行器呢?
public CloseableHttpClient build() { //省略一些代碼 // Add request retry executor, if not disabled if (!automaticRetriesDisabled) { HttpRequestRetryHandler retryHandlerCopy = this.retryHandler; if (retryHandlerCopy == null) { retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE; } execChain = new RetryExec(execChain, retryHandlerCopy); } }
能夠看到在build() httpclient實例的時候,判斷了是否關閉了自動重試,這個automaticRetriesDisabled類型是boolean,默認值是false,因此if這裏是知足的。
即若是沒有指定執行鏈,就是用RetryExec執行器,默認的重試策略是DefaultHttpRequestRetryHandler。
前面已經看到咱們使用的HttiClient本質上是InternalHttpClient,這裏看下他的執行發送數據的方法。
@Override protected CloseableHttpResponse doExecute( final HttpHost target, final HttpRequest request, final HttpContext context) throws IOException, ClientProtocolException { //省略一些代碼
return this.execChain.execute(route, wrapper, localcontext, execAware); } }
最後一行能夠看到,最終的執行execute方式使用的是exeChain的執行方法,而execChain是經過InternalHttpClient構造器傳進來的,就是上面看到的RetryExec。
因此,HttpClient有默認的執行器RetryExec,其默認的重試策略是DefaultHttpRequestRetryHandler。
http請求是執行器執行的,因此先看RetryExec發送請求的部分。
public CloseableHttpResponse execute( final HttpRoute route, final HttpRequestWrapper request, final HttpClientContext context, final HttpExecutionAware execAware) throws IOException, HttpException { //參數校驗 Args.notNull(route, "HTTP route"); Args.notNull(request, "HTTP request"); Args.notNull(context, "HTTP context"); final Header[] origheaders = request.getAllHeaders(); //這個for循環記錄了當前http請求的執行次數 for (int execCount = 1;; execCount++) { try { //調用基礎executor執行http請求 return this.requestExecutor.execute(route, request, context, execAware); } catch (final IOException ex) { //發生IO異常的時候,判斷上下文是否已經中斷,若是中斷則拋異常退出 if (execAware != null && execAware.isAborted()) { this.log.debug("Request has been aborted"); throw ex; } //根據重試策略,判斷當前執行情況是否要重試,若是是則進入下面邏輯 if (retryHandler.retryRequest(ex, execCount, context)) { //日誌 if (this.log.isInfoEnabled()) { this.log.info("I/O exception ("+ ex.getClass().getName() + ") caught when processing request to " + route + ": " + ex.getMessage()); } //日誌 if (this.log.isDebugEnabled()) { this.log.debug(ex.getMessage(), ex); } //判斷當前請求是否能夠被重複發起 if (!RequestEntityProxy.isRepeatable(request)) { this.log.debug("Cannot retry non-repeatable request"); throw new NonRepeatableRequestException("Cannot retry request " + "with a non-repeatable request entity", ex); } request.setHeaders(origheaders); if (this.log.isInfoEnabled()) { this.log.info("Retrying request to " + route); } } else { //若是重試策略判斷不能重試了,則根據異常狀態拋異常,退出當前流程 if (ex instanceof NoHttpResponseException) { final NoHttpResponseException updatedex = new NoHttpResponseException( route.getTargetHost().toHostString() + " failed to respond"); updatedex.setStackTrace(ex.getStackTrace()); throw updatedex; } else { throw ex; } } } } }
關於RetryExec執行器的執行過程,作一個階段小結:
在上文咱們看到了默認的重試策略是DefaultHttpRequestRetryHandler.INSTANCE。
//單例模式 public static final DefaultHttpRequestRetryHandler INSTANCE = new DefaultHttpRequestRetryHandler(); //重試次數 private final int retryCount; //若是一個請求發送成功過,是否還會被再次發送 private final boolean requestSentRetryEnabled; private final Set<Class<? extends IOException>> nonRetriableClasses; public DefaultHttpRequestRetryHandler() { this(3, false); } public DefaultHttpRequestRetryHandler(final int retryCount, final boolean requestSentRetryEnabled) { this(retryCount, requestSentRetryEnabled, Arrays.asList( InterruptedIOException.class, UnknownHostException.class, ConnectException.class, SSLException.class)); } protected DefaultHttpRequestRetryHandler( final int retryCount, final boolean requestSentRetryEnabled, final Collection<Class<? extends IOException>> clazzes) { super(); this.retryCount = retryCount; this.requestSentRetryEnabled = requestSentRetryEnabled; this.nonRetriableClasses = new HashSet<Class<? extends IOException>>(); for (final Class<? extends IOException> clazz: clazzes) { this.nonRetriableClasses.add(clazz); } }
經過構造器能夠看到,默認的重試策略是:
說句題外話,這是一個單例模式,屬於餓漢模式。
餓漢模式的缺點是,這個類在被加載的時候就會初始化這個對象,對內存有佔用。不過這個對象維護的filed比較小,因此對內存的影響不大。
另外因爲這個類全部的field都是final的,因此是一個不可變的對象,是線程安全的。
public boolean retryRequest( final IOException exception, final int executionCount, final HttpContext context) { //參數校驗 Args.notNull(exception, "Exception parameter"); Args.notNull(context, "HTTP context"); //若是已經執行的次數大於設置的次數,則不繼續重試 if (executionCount > this.retryCount) { return false; } //若是是上面規定的幾種異常,則不重試 if (this.nonRetriableClasses.contains(exception.getClass())) { return false; } else { //若是是上面規定的集中異常的子類,則不重試 for (final Class<? extends IOException> rejectException : this.nonRetriableClasses) { if (rejectException.isInstance(exception)) { return false; } } } final HttpClientContext clientContext = HttpClientContext.adapt(context); final HttpRequest request = clientContext.getRequest(); //判斷當前請求是否已經被終止了,這個是避免當前請求被放入異步的異步的HttpRequestFutureTask中 //跟進去能夠看到,當這個異步任務被cancel的時候,會經過AtomicBoolean的compareAndSet的方法,保證狀態被更改 //這部分不作詳細討論了 if(requestIsAborted(request)){ return false; } //判斷請求是不是冪等請求,跟進去能夠看到,全部包含http body的請求都認爲是非冪等的,好比post/put等 //冪等的請求能夠直接重試,好比get if (handleAsIdempotent(request)) { return true; } //根據上下文判斷請求是否發送成功了,或者根據狀態爲是否永遠能夠重複發送(默認的是否) //這個下面會分析 if (!clientContext.isRequestSent() || this.requestSentRetryEnabled) { return true; } //不然不須要重試 return false; } }
關於默認的重試策略,作一個階段小結:
那麼關鍵問題來了,如何判斷請求是否已經發送成功了呢?
public static final String HTTP_REQ_SENT = "http.request_sent"; public boolean isRequestSent() { final Boolean b = getAttribute(HTTP_REQ_SENT, Boolean.class); return b != null && b.booleanValue(); }
可看到若是當前的httpContext中的http.request_sent屬性爲true,則認爲已經發送成功,不然認爲尚未發送成功。
那麼就剩下一個問題了,一次正常的http請求中http.request_sent屬性是若是設置的?
上面有提到過,RetryExec在底層通訊使用了MainClientExec,而MainCLientExec底層調用了HttpRequestExecutor.doSendRequest()
protected HttpResponse doSendRequest( final HttpRequest request, final HttpClientConnection conn, final HttpContext context) throws IOException, HttpException { Args.notNull(request, "HTTP request"); Args.notNull(conn, "Client connection"); Args.notNull(context, "HTTP context"); HttpResponse response = null; context.setAttribute(HttpCoreContext.HTTP_CONNECTION, conn); //首先在請求發送以前,將http.request_sent放入上下文context的屬性中,值爲false context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.FALSE); //將request的Header放入鏈接中 conn.sendRequestHeader(request); //若是是post/put這種有body的請求,須要先判斷100-cotinue擴展協議是否支持 //即發送包含body請求前,先判斷服務端是否支持一樣的協議若是不支持,則不發送了。除非特殊約定,默認雙端是都不設置的。 if (request instanceof HttpEntityEnclosingRequest) { boolean sendentity = true; final ProtocolVersion ver = request.getRequestLine().getProtocolVersion(); if (((HttpEntityEnclosingRequest) request).expectContinue() && !ver.lessEquals(HttpVersion.HTTP_1_0)) { conn.flush(); if (conn.isResponseAvailable(this.waitForContinue)) { response = conn.receiveResponseHeader(); if (canResponseHaveBody(request, response)) { conn.receiveResponseEntity(response); } final int status = response.getStatusLine().getStatusCode(); if (status < 200) { if (status != HttpStatus.SC_CONTINUE) { throw new ProtocolException( "Unexpected response: " + response.getStatusLine()); } // discard 100-continue response = null; } else { sendentity = false; } } } //若是能夠發送,則將body序列化後,寫入當前流中 if (sendentity) { conn.sendRequestEntity((HttpEntityEnclosingRequest) request); } } //刷新當前鏈接,發送數據 conn.flush(); //將http.request_sent置爲true context.setAttribute(HttpCoreContext.HTTP_REQ_SENT, Boolean.TRUE); return response; }
上面是一個完成的http通訊部分,步驟以下:
顯然,對於conn.flush()這一步是會發生異常的,這種狀況下就認爲沒有發送成功。
說句題外話,上面對coon的操做都是基於鏈接池的,每次都是從池中拿到一個可用鏈接。
對於咱們的場景應用中的get與post,能夠總結爲:
首先分析下不重試的異常:
另外,咱們還常常會提到兩種超時,鏈接超時與讀超時:
這兩種超時都是SocketTimeoutException,繼承自InterruptedIOException,屬於上面的第1種線程中斷異常,不會進行重試。
對於大多數系統而言,不少交互都是經過post的方式與第三方交互的。
因此,咱們須要知道有哪些狀況HttpClient給咱們進行了默認重試。
咱們關心的場景轉化爲,post請求在輸出流進行write與flush的時候,會發生哪些除了InterruptedIOException、UnknownHostException、ConnectException、SSLException之外的IOExecetion。
可能出問題的一步在於HttpClientConnection.flush()的一步,跟進去能夠得知其操做的對象是一個SocketOutputStream,而這個類的flush是空實現,因此只須要看wirte方法便可。
private void socketWrite(byte b[], int off, int len) throws IOException { if (len <= 0 || off < 0 || len > b.length - off) { if (len == 0) { return; } throw new ArrayIndexOutOfBoundsException("len == " + len + " off == " + off + " buffer length == " + b.length); } FileDescriptor fd = impl.acquireFD(); try { socketWrite0(fd, b, off, len); } catch (SocketException se) { if (se instanceof sun.net.ConnectionResetException) { impl.setConnectionResetPending(); se = new SocketException("Connection reset"); } if (impl.isClosedOrPending()) { throw new SocketException("Socket closed"); } else { throw se; } } finally { impl.releaseFD(); } }
能夠看到,這個方法會拋出IOExecption,代碼中對SocketException異常進行了加工。從以前的分析中能夠得知,SocketException是不在能夠忽略的範圍內的。
因此從上面代碼上就能夠分析得出對於傳輸過程當中socket被重置或者關閉的時候,httpclient會對post請求進行重試。
以及一些其餘的IOExecption也會進行重試,不過範圍過廣很差定位。
回到HttpClientBuilder中,其build()方法中之因此選擇了RetryExec執行器是有前置條件的,即沒有手動禁止。
// Add request retry executor, if not disabled if (!automaticRetriesDisabled) { HttpRequestRetryHandler retryHandlerCopy = this.retryHandler; if (retryHandlerCopy == null) { retryHandlerCopy = DefaultHttpRequestRetryHandler.INSTANCE; } execChain = new RetryExec(execChain, retryHandlerCopy); }
因此咱們在構建httpClient實例的時候手動禁止掉便可。
/** * Disables automatic request recovery and re-execution. */ public final HttpClientBuilder disableAutomaticRetries() { automaticRetriesDisabled = true; return this; }
經過本文分析,能夠得知HttpClient默認是有重試機制的,其重試策略是:
1.只有發生IOExecetion時纔會發生重試
2.InterruptedIOException、UnknownHostException、ConnectException、SSLException,發生這4中異常不重試
3.get方法能夠重試3次,post方法在socket對應的輸出流沒有被write並flush成功時能夠重試3次。
4.讀/寫超時不進行重試
5.socket傳輸中被重置或關閉會進行重試
6.以及一些其餘的IOException,暫時分析不出來。