Java 中 HttpURLConnection 與 PoLA 法則
本文由碼農網 – Sandbox Wang原創翻譯,轉載請看清文末的轉載要求,歡迎參與我們的付費投稿計劃!
引言:如果你也是開發者的話,你很可能已經知道PoLA法則(Principle of Lease Astonishment)。那麼,看看這篇文章講述的充滿奇幻色彩的除錯經歷,來見識一下PoLA是如何與HttpURLConnection發生了關聯。
如果你和我一樣也是開發者的話,你很可能已經聽說過“PoLA”原則,或者叫作“產生最少意外”原則。意思非常簡單,就是不要讓你的使用者感到驚訝。或者更明確一些,就像本文這種情況,不要讓另外一個開發者感到驚訝。不幸的是,我上個星期就遇到了大大超出我意外的事情,我們有個服務的客戶呼叫端總是發出一些垃圾的請求。
你說垃圾請求嗎?是的,就像這樣,我們完全不清楚這些請求是從哪裡來的。又是這樣一個時刻,經理們毫無頭緒,抱頭亂竄,驚呼“我們肯定是被黑客攻擊了”,或者 ”有人把防火牆給關掉了!!”
無論如何,先說點背景情況吧,我們的專案裡有自動記錄活動日誌的功能,當某些情況下,比如一個程式啟動的時候就會進行記錄。這包括我們那出問題的網路服務客戶端和服務端,因為它們兩者都屬於系統的一部分。在某些時候,我們注意到,服務端的響應還沒有發出的時候,另外一個來自同樣客戶端的請求又發了過來。這個真是出乎意料的,因為客戶端程式碼是單執行緒的,也沒有其他的客戶端摻和進來。審查程式碼、測試之後,結論是我們的客戶端不可能在第一個請求還沒結束的時候再同時發出另外一個。
經過一整天的除錯和研究日誌發現,事實上,在服務端處理還未結束的時候客戶端其實已經斷開連線了。所以,這些請求終究並不是同時發生的,但是為什麼我們花了一整天的時間才發現呢?這跟我們玩了一整天的星球大戰有啥區別?
好吧,其實也不是。我們發現了罪魁禍首,服務端的容器軟體HTTP的讀超時設定被調得太低了。服務端的日誌顯示的確生成了響應,但是客戶端卻在此之前已經斷開了,因為伺服器端發生了讀超時。這些在伺服器端當然沒有日誌記錄,因為這種行為是更低一層協議決定的(HTTP棧),而不是服務端的應用程式碼。
是的,沒錯,我聽明白了,但是客戶端的日誌該怎麼解釋?客戶端是不是應該丟擲一個“ReadTimeoutException”異常,或者類似的玩意,然後可以寫到日誌裡?然而,沒錯,事實上,並沒有。就像現在發現的一樣,真正的意外來自HttpURLConnection類的內部(更確切地說,是預設的Oracle的官方實現sun.net.www.protocol.http.HttpURLConnection)。
你以前是否知道HttpURLConnection的預設實現有個在某些情形下自動重試的特性?好吧,我之前就不知道。當時的情況是,客戶端的確觸發了超時異常,但是卻被HttpURLConnection給捕捉了,而它自己決定重新嘗試一次。這就意味著,你呼叫了HttpURLConnection的read()方法,它阻塞了,你正在等待,看起來就好像是在等待第一次請求的響應一樣。但是在HttpURLConnection內部,它作了不止一次嘗試,因此建立了不止一個socket連線。這就解釋了為什麼第二次及以後的請求永遠在日誌裡找不到,因為這些第二次之後的請求是HttpURLConnection內部發起的。
讓我們上一些程式碼重現一下。
import java.net.HttpURLConnection; import java.net.InetSocketAddress; import java.net.SocketTimeoutException; import java.net.URL; import java.util.concurrent.Executors; import com.sun.net.httpserver.HttpServer; /** * Created by koen on 30/01/16. */ public class TestMe { public static void main(String[] args) throws Exception { startHttpd(); HttpURLConnection httpURLConnection = (HttpURLConnection) new URL("http://localhost:8080/").openConnection(); if (!(httpURLConnection instanceof sun.net.www.protocol.http.HttpURLConnection)) { throw new IllegalStateException("Well it should really be sun.net.www.protocol.http.HttpURLConnection. " + "Check if no library registered it's impl using URL.setURLStreamHandlerFactory()"); } httpURLConnection.setRequestMethod("POST"); httpURLConnection.connect(); System.out.println("Reading from stream..."); httpURLConnection.getInputStream().read(); System.out.println("Done"); } public static void startHttpd() throws Exception { InetSocketAddress addr = new InetSocketAddress(8080); HttpServer server = HttpServer.create(addr, 0); server.createContext("/", httpExchange -> { System.out.println("------> Httpd got request. Request method was:" + httpExchange.getRequestMethod() + " Throwing timeout exception"); if (true) { throw new SocketTimeoutException(); } }); server.setExecutor(Executors.newCachedThreadPool()); server.start(); System.out.println("Open for business."); } }
執行之,將會得到類似下面的輸出。
Open for business. Reading from stream... ------> Httpd got request. Request method was:POST Throwing timeout exception ------> Httpd got request. Request method was:POST Throwing timeout exception Exception in thread "main" java.net.SocketException: Unexpected end of file from server at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792) ...
注意,我們的監聽服務被呼叫了兩次,但是我們只發了一個請求。如果我們加上-Dsun.net.http.retryPost=false這個屬性再執行一次的話,我們會得到下面的輸出:
------> Httpd got request. Request method was:POST Throwing timeout exception Exception in thread "main" java.net.SocketException: Unexpected end of file from server at sun.net.www.http.HttpClient.parseHTTPHeader(HttpClient.java:792) ...
好,先把這事放一邊,我想問的是,到底是誰搞出這麼個設計來,既沒文件描述又沒有可配置選項?為啥我做了十五年的Java開發,卻對此一無所知?更要命的是,為什麼它要對一個構造異常的POST請求進行重試呢?這是對PoLA赤裸裸的違背!
現在你可能已經猜到了,這是一個BUG(連結:http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6382788)。當然了,說是BUG並不是指的它的重試機制,而是指它為什麼對異常POST請求也會進行重試。按照HTTP RFC的規範,POST請求並非冪等,因此多次提交POST會帶來伺服器端資料的改變。但是別擔心,Bill早就把這個BUG修改好了。Bill的解決方法是加了一個開關。Bill瞭解向後相容原則。Bill認為最好的方法是新增一個預設開啟的開關,這樣可以保證這個BUG的向後相容。Bill笑了。Bill已經能夠看見全球無數的Java開發者掉進這個大坑時驚愕的面孔。但是,你們都別學Bill好嗎?
經過好幾天激動人心的除錯,最後問題解決的方式卻略顯輕巧,僅僅指定了一個屬性就搞定了。無論如何,這個設計真是著實讓我很意外,因此我還專門寫了這篇文章來講述,並且,你也看到了這篇文章。
為了完整起見,再提醒一下,如果你讓這段程式碼在容器環境裡執行的話,結果可能會不同。你的容器或者你的程式碼所依賴的庫有可能會替換掉Oracle預設的內部實現,請參考URL.setURLStreamHandlerFactory()。現在你可能會問,那個傢伙當時為什麼要使用HttpURLConnection呢?他難道是坐著演講巡遊車上班嗎(原文Wooden Soapbox,由來參見https://en.wikipedia.org/wiki/Soapbox)?他難道是用剪子來割草嗎?建議他傳遞資訊的時候最好還是使用烽火吧!當然了,你這麼想我也不能責怪你。我們出問題的程式碼有點特別,使用的是SAAJ中的SOAPConnectionFactory,而SOAPConnectionFactory內部又預設使用了HttpURLConnection,如果沒有其他程式碼來註冊其他的實現類的話,使用的當然就是預設的Oracle實現嘍~
如果你使用其他更專業的web服務實現的時候(如Spring WS, CXF, JAX-WS實現等等),他們很可能使用了諸如Apache HTTP Client的元件。當然了,如果你自己的程式碼需要發起HTTP連線的話,你也可以使用它。沒錯,我還是推薦你使用Apache Commons HttpClient,雖然這貨修改API的頻率比普通時尚達人換鞋的頻率都還要高。好了,我的牢騷完了。
譯文連結:http://www.codeceo.com/article/java-httpurlconnection-pola.html
英文原文:HttpURLConnection vs. the Principle of Least Astonishment
翻譯作者:碼農網 – Sandbox Wang
[ 轉載必須在正文中標註並保留原文連結、譯文連結和譯者等資訊。]
相關文章
- RAII與三五零法則AI
- JAVA語法規則 (轉)Java
- Android 中 HttpURLConnection 使用詳解AndroidHTTP
- Android中HttpURLConnection使用詳解AndroidHTTP
- Java中的介面與抽象類設計原則Java抽象
- 漫遊HttpURLConnectionHTTP
- AndroidHttp通訊 HTTP Client與HttpURLConnection的區別AndroidHTTPclient
- java中url正則regex匹配Java
- HTML基本語法和語義寫法規則與例項HTML
- 【java規則引擎】規則引擎RuleBase中利用觀察者模式Java模式
- Builder模式與Java語法UI模式Java
- 淺析JAVA日誌中的幾則效能實踐與原理解釋Java
- 單變數微分、導數與鏈式法則變數
- Android HttpURLConnection詳解AndroidHTTP
- HttpURLConnection和HttpClient的使用HTTPclient
- Java中的設計模式和原則Java設計模式
- PolarDB 資料庫效能大賽 Java 分享資料庫Java
- 二八法則在軟體設計中可行嗎?
- 正則語法
- 【大資料】大資料企業策略與法則大資料
- 概率的意義:隨機世界與大數法則隨機
- 洛必達法則的證明與可用條件
- Java 中的語法糖,真甜。Java
- Java中動態規則的實現方式Java
- Java中物件導向的設計原則Java物件
- IT故障排查工作中的六條不變法則
- 介面設計中七個不能違反的法則
- 雙均線策略:量化交易中的黃金法則
- Sass 語法規則
- 定位法則(轉載)
- PolarCode
- polardb 部署
- 高效能工作流引擎:DataBuilder與polarisUI
- 【java規則引擎】基本語法和相關屬性介紹Java
- java中的try-with-resource語法Java
- PolarDB資料庫效能大賽Java選手分享資料庫Java
- Java中23種設計模式:六大設計原則的分析與介紹Java設計模式
- Java使用正則獲取字串中匹配欄位Java字串