給 JDK 報(bào)了一個(gè) P4 的 Bug,結(jié)果居然……

分享一下之前踩的一個(gè)坑,背景是這樣的:
我們的項(xiàng)目依賴于一個(gè)外部服務(wù),該外部服務(wù)提供 REST 接口供我方調(diào)用,這是很常見(jiàn)的一個(gè)場(chǎng)景。本地和測(cè)試環(huán)境測(cè)試都沒(méi)有問(wèn)題,一切就緒上了生產(chǎn)后,程序調(diào)用接口就總是網(wǎng)絡(luò)不通。
需要說(shuō)明的是本地、測(cè)試環(huán)境、生產(chǎn)環(huán)境通過(guò)不同的域名訪問(wèn)該外部服務(wù)。生產(chǎn)程序調(diào)用不通,神奇的是在生產(chǎn)環(huán)境通過(guò)?curl?等命令卻能夠正常調(diào)用對(duì)方接口。

這 TM 就神奇了,唯一不同的就是發(fā)起 HTTP 請(qǐng)求的客戶端了,估計(jì)就是 http客戶端有問(wèn)題了?通過(guò)最后排查發(fā)現(xiàn),居然發(fā)現(xiàn)了一枚 “JDK 的 bug”,然后石頭就提交到了 JDK 的官網(wǎng)……

下面我們就來(lái)重現(xiàn)一下這個(gè)問(wèn)題。
server 端準(zhǔn)備
這里用 Nginx 模擬了一下 上文提到的 REST 服務(wù),假設(shè)調(diào)用正常返回?"Hello, World\n",Nginx 配置如下:
server?{
????listen????80;
????server_name?test_1.tanglei.name;
????location?/testurl?{
????????add_header?Content-Type?'text/plain;?charset=utf-8';
????????return?200?"Hello,?World\n";
????}
}
不同的 client 請(qǐng)求
下面用不同的 Http client (分別用命令行curl,python的?requests包,和 Java 的 URL 等嘗試)去請(qǐng)求。
curl?請(qǐng)求,正常。
[root@VM_77_245_centos?vhost]#?curl?-i?"http://test_1.tanglei.name/testurl"
HTTP/1.1?200?OK
Server:?nginx
Content-Length:?13
Connection:?keep-alive
Content-Type:?text/plain;?charset=utf-8
Hello,?World
[root@VM_77_245_centos?vhost]#
python requests?正常。
>>>?import?requests
>>>?r?=?requests.get("http://test_1.tanglei.name/testurl")
>>>?r.text
u'Hello,?World\n'
Java 的? java.net.URLConnection?同樣正常。
static?String?getContent(java.net.URL?url)?throws?Exception?{
????java.net.URLConnection?conn?=?url.openConnection();
????java.io.InputStreamReader?in?=?new?java.io.InputStreamReader(conn.getInputStream(),?"utf-8");
????java.io.BufferedReader?reader?=?new?java.io.BufferedReader(in);????
????StringBuilder?sb?=?new?StringBuilder();
????int?c?=?-1;
????while?((c?=?reader.read())?!=?-1)?{
????????sb.append((char)c);
????}
????reader.close();
????in.close();
????String?response?=?sb.toString();
????return?response;
}
上面的這個(gè)方法?String getContent(java.net.URL url)?傳入一個(gè)構(gòu)造好的?java.net.URL?然后 get 請(qǐng)求,并以?String?方式返回 response。
String?srcUrl?=?"http://test_1.tanglei.name/testurl";
java.net.URL?url?=?new?java.net.URL(srcUrl);
System.out.println("\nurl?result:\n"?+?getContent(url));?//?OK
上面的語(yǔ)句輸出正常,結(jié)果如下:
url?result:
Hello,?World
這就尼瑪神奇了吧??纯次覀兂绦蛑杏玫?httpclient 的實(shí)現(xiàn),結(jié)果發(fā)現(xiàn)是有用?java.net.URI,心想,這不至于吧,用 URI 就不行了么。

換?java.net.URI?試試? (這里不展開(kāi)講URL和URI的區(qū)別聯(lián)系了,可以簡(jiǎn)單的認(rèn)為URL是URI的一個(gè)子集,詳細(xì)的可參考?URI、URL 和 URN[1],?wiki URI[2])
直接通過(guò)java.net.URI構(gòu)造,再調(diào)用?URI.toURL?得到?URL,調(diào)用同樣正常。
關(guān)鍵的來(lái)了,httpclient 源碼中用的構(gòu)造函數(shù)是另外一個(gè):
URI(String?scheme,?String?host,?String?path,?String?fragment)
Constructs?a?hierarchical?URI?from?the?given?components.
我用這個(gè)方法構(gòu)造URI,會(huì)構(gòu)造失?。?/p>
new?java.net.URI(uri.getScheme(),?uri.getHost(),?uri.getPath(),?null)?error:?protocol?=?http?host?=?null
new?java.net.URI(url.getProtocol(),?url.getHost(),?url.getPath(),?null)?error:?Illegal?character?in?hostname?at?index?11:?http://test_1.tanglei.name/testurl
所以問(wèn)題發(fā)現(xiàn)了,我們的項(xiàng)目中依賴的第三方 httpclient包底層用到了?java.net.URI,恰好在?java.net.URI?中是不允許以下劃線(_)作為?hostname?字段的。
即?uri.getHost()?和?uri.toURL().getHost()?居然能不相等。
這是 JDK 的 Bug 吧?
有理由懷疑,這是 JDK 的 Bug 吧?

從官網(wǎng)上還真找到了關(guān)于包含下劃線作為hostname的bug提交issue,戳這里 JDK-8132508 : Bug JDK-8029354 reproduces with underscore in hostname[3],然后發(fā)現(xiàn)該 "bug" reporter 的情況貌似跟我的差不多,只不過(guò)引爆bug的點(diǎn)不一樣。
該 "bug" reviewer 最后以 "Not an Issue" 關(guān)閉,給出的理由是:
RFC 952 disallows _ underscores in hostnames. So, this is not a bug.
確實(shí),rfc952[4]?明確明確說(shuō)了域名只能由 字母?(A-Z)、 數(shù)字(0-9)、 減號(hào)?(-)?和 點(diǎn)?(.)?組成。
那 OK 吧,既然明確規(guī)定了 hostname 不能包含下劃線,為啥?java.net.URL?確允許呢?
造成?java.net.URI?和?java.net.URL?在處理 hostname 時(shí)的標(biāo)準(zhǔn)不一致,且本身?java.net.URI?在構(gòu)造的時(shí)候也帶了 "有色"眼鏡,同一個(gè)url字符串 通過(guò)靜態(tài)方法?java.net.URI.create(String)?或者通過(guò)帶1個(gè)參數(shù)的構(gòu)造方法?java.net.URI(String)?都能成功構(gòu)造出 URI 的實(shí)例,但通過(guò)帶4個(gè)參數(shù)的構(gòu)造方法就不能構(gòu)造了。
要知道,在 coding 過(guò)程中,盡早反饋異常信息更有利于軟件開(kāi)發(fā)持續(xù)迭代的過(guò)程。我們?cè)陂_(kāi)發(fā)過(guò)程中也應(yīng)該遵循這一點(diǎn)原則。
于是我就去 JDK 官網(wǎng)提交了一個(gè) bug,大意是說(shuō)?java.net.URI?和?java.net.URL?在處理hostname的時(shí)候標(biāo)準(zhǔn)不一致,容易使開(kāi)發(fā)人員埋藏一些潛在的bug,同時(shí)也還把這個(gè)問(wèn)題反饋到?stackoverflow[5]?了
I am wondering, if hostname with underscore is not valid, why the result is differrent between java.net.URI and java.net.URL? Is it a bug or a feature? Here is the example.
java.net.URL url = new java.net.URL("http://test_1.tanglei.name");?
System.out.println(url.getHost()); //test_1.tanglei.name?
java.net.URI uri = new java.net.URI("http://test_1.tanglei.name");?
System.out.println(uri.getHost()); //null
這個(gè) JDK bug issue 詳細(xì)信息見(jiàn)?JDK-8170265 : underscore is allowed in java.net.URL while not in java.net.URI[6],openjdk JDK-8170265[7]

經(jīng)過(guò)初步 Review,被認(rèn)為是一個(gè) P4 的 Bug,說(shuō)的是?java.net.URL?遵循的是?RFC 2396?規(guī)范,確實(shí)不允許含有下劃線的 hostname,java.net.URI?做到了, 而?java.net.URL?沒(méi)有做到。

重點(diǎn)來(lái)了,然后,卻被另外一個(gè) Reviewer 直接個(gè)斃了。給出的原因是?java.net.URL?構(gòu)造方法中,API 文檔中說(shuō)了本來(lái)也不會(huì)做驗(yàn)證即?No validation of the inputs is performed by this constructor.??在線 api doc 戳這里[8]?(可以點(diǎn)連接,進(jìn)去搜索關(guān)鍵字 "No validation")


當(dāng)初沒(méi)有收到及時(shí)反饋,就沒(méi)有來(lái)得及懟回去。
其實(shí)就算 "No validation of the inputs is performed by this constructor." 是合理的,里面也只有3個(gè)構(gòu)造函數(shù)有這樣的說(shuō)明,按照這樣的邏輯是不是說(shuō)另外的構(gòu)造函數(shù)有驗(yàn)證呢..... (示例中的默認(rèn)的構(gòu)造函數(shù)都沒(méi)有說(shuō)呀)
這里有java.net.URL 的源碼[9],看興趣的同學(xué)可以看看。
恩,以上就是結(jié)論了。
不過(guò),反正我自己感覺(jué)目前 Java API 關(guān)于這里的設(shè)計(jì)不太合理,歡迎大家討論。
我在SO提問(wèn)的這個(gè)回答[10]比較有意思,哈哈。
The review is somewhat terse, but the reviewer's point is the URL constructor is behaving in accordance with its specification. Since the specification explicitly states that no validation is performed, this is not a bug in the code. This is indisputable.
What he didn't spell out is that fixing this inconsistency (by changing the URL class specification) would break lots of peoples' 20+ year old code Java code. That would be a really bad idea. It can't happen.
So ... this inconsistency is a "feature"

