Java URL類踩坑指南

背景介紹

最近再做一個RSS閱讀工具給自己用,其中一個環(huán)節(jié)是從服務(wù)器端獲取一個包含了RSS源列表的json文件,再根據(jù)這個json文件下載、解析RSS內(nèi)容悠砚。核心代碼如下:

class PresenterImpl(val context: Context, val activity: MainActivity) : IPresenter {
    private val URL_API = "https://vimerzhao.github.io/others/rssreader/RSS.json"

    override fun getRssResource(): RssSource {
        val gson = GsonBuilder().create()
        return gson.fromJson(getFromNet(URL_API), RssSource::class.java)
    }

    private fun getFromNet(url: String): String {
        val result = URL(url).readText()
        return result
    }

    ......
}

之前一直執(zhí)行地很好,直到前兩天我購買了一個vimerzhao.top的域名偷仿,并將原來的域名vimerzhao.github.io重定向到了vimerzhao.top哩簿。這個工具就無法使用了,但在瀏覽器輸入URL_API卻能得到數(shù)據(jù):

image

那為什么URL.readText()沒有拿到數(shù)據(jù)呢酝静?

不支持重定向

可以通過下面代碼測試:

import java.net.*;
import java.io.*;

public class TestRedirect {
    public static void main(String args[]) {
        try {
            URL url1 = new URL("https://vimerzhao.github.io/others/rssreader/RSS.json");
            URL url2 = new URL("http://vimerzhao.top/others/rssreader/RSS.json");
            read(url1);
            System.out.println("=--------------------------------=");
            read(url2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void read(URL url) {
        try {
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(url.openStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

得到結(jié)果如下:

<html>
<head><title>301 Moved Permanently</title></head>
<body bgcolor="white">
<center><h1>301 Moved Permanently</h1></center>
<hr><center>nginx</center>
</body>
</html>
=--------------------------------=
{"theme":"tech","author":"zhaoyu","email":"dutzhaoyu@gmail.com","version":"0.01","contents":[{"category":"綜合版塊","websites":[{"tag":"門戶網(wǎng)站","url":["http://geek.csdn.net/admin/news_service/rss","http://blog.jobbole.com/feed/","http://feed.cnblogs.com/blog/sitehome/rss","https://segmentfault.com/feeds","http://www.codeceo.com/article/category/pick/feed"]},{"tag":"知名社區(qū)","url":["https://stackoverflow.com/feeds","https://www.v2ex.com/index.xml"]},{"tag":"官方博客","url":["https://www.blog.google/rss/","https://blog.jetbrains.com/feed/"]},{"tag":"個人博客-行業(yè)","url":["http://feed.williamlong.info/","https://www.liaoxuefeng.com/feed/articles"]},{"tag":"個人博客-學(xué)術(shù)","url":["http://www.norvig.com/rss-feed.xml"]}]},{"category":"編程語言","websites":[{"tag":"Kotlin","url":["https://kotliner.cn/api/rss/latest"]},{"tag":"Python","url":["https://www.python.org/dev/peps/peps.rss/"]},{"tag":"Java","url":["http://www.codeceo.com/article/category/develop/java/feed"]}]},{"category":"行業(yè)動態(tài)","websites":[{"tag":"Android","url":["http://www.codeceo.com/article/category/develop/android/feed"]}]},{"category":"亂七八遭","websites":[{"tag":"Linux-綜合","url":["https://linux.cn/rss.xml","http://www.linuxidc.com/rssFeed.aspx","http://www.codeceo.com/article/tag/linux/feed"]},{"tag":"Linux-發(fā)行版","url":["https://blog.linuxmint.com/?feed=rss2","https://manjaro.github.io/feed.xml"]}]}]}

HTTP返回碼301节榜,即發(fā)生了重定向”鹬牵可在瀏覽器上這個過程太快以至于我們看不到這個301界面的出現(xiàn)宗苍。這里需要說明的是URL.readText()是Kotlin中一個擴(kuò)展函數(shù),本質(zhì)還是調(diào)用了URL類的openStream方法薄榛,部分源碼如下:

.....
/**
 * Reads the entire content of this URL as a String using UTF-8 or the specified [charset].
 *
 * This method is not recommended on huge files.
 *
 * @param charset a character set to use.
 * @return a string with this URL entire content.
 */
@kotlin.internal.InlineOnly
public inline fun URL.readText(charset: Charset = Charsets.UTF_8): String = readBytes().toString(charset)

/**
 * Reads the entire content of the URL as byte array.
 *
 * This method is not recommended on huge files.
 *
 * @return a byte array with this URL entire content.
 */
public fun URL.readBytes(): ByteArray = openStream().use { it.readBytes() }

所以上面的測試代碼即說明了URL.readText()失敗的原因讳窟。
不過URL不支持重定向是否合理?為什么不支持敞恋?還有待探究窥突。

不穩(wěn)定的equals方法

首先看下equals的說明(URL (Java Platform SE 7 )):

Compares this URL for equality with another object.
If the given object is not a URL then this method immediately returns false.
Two URL objects are equal if they have the same protocol, reference equivalent hosts, have the same port number on the host, and the same file and fragment of the file.
Two hosts are considered equivalent if both host names can be resolved into the same IP addresses; else if either host name can't be resolved, the host names must be equal without regard to case; or both host names equal to null.
Since hosts comparison requires name resolution, this operation is a blocking operation.
Note: The defined behavior for equals is known to be inconsistent with virtual hosting in HTTP.

接下來再看一段代碼:

import java.net.*;
public class TestEquals {
    public static void main(String args[]) {
        try {
            // vimerzhao的博客主頁
            URL url1 = new URL("https://vimerzhao.github.io/");
            // zhanglanqing的博客主頁
            URL url2 = new URL("https://zhanglanqing.github.io/");
            // vimerzhao博客主頁重定向后的域名
            URL url3 = new URL("http://vimerzhao.top/");
            System.out.println(url1.equals(url2));
            System.out.println(url1.equals(url3));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

根據(jù)定義輸出結(jié)果是什么呢孔祸?運(yùn)行之后是這樣:

true
false

你可能猜對了,但如果我把電腦斷網(wǎng)之后再次執(zhí)行,結(jié)果卻是:

false
false

但其實(shí)3個域名的IP地址都是相同的凳谦,可以ping一下:

zhaoyu@Inspiron ~/Project $ ping vimezhao.github.io
PING sni.github.map.fastly.net (151.101.77.147) 56(84) bytes of data.
64 bytes from 151.101.77.147: icmp_seq=1 ttl=44 time=396 ms
^C
--- sni.github.map.fastly.net ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 396.692/396.692/396.692/0.000 ms
zhaoyu@Inspiron ~/Project $ ping zhanglanqing.github.io
PING sni.github.map.fastly.net (151.101.77.147) 56(84) bytes of data.
64 bytes from 151.101.77.147: icmp_seq=1 ttl=44 time=396 ms
^C
--- sni.github.map.fastly.net ping statistics ---
2 packets transmitted, 1 received, 50% packet loss, time 1000ms
rtt min/avg/max/mdev = 396.009/396.009/396.009/0.000 ms
zhaoyu@Inspiron ~/Project $ ping vimezhao.top
ping: unknown host vimezhao.top
zhaoyu@Inspiron ~/Project $ ping vimerzhao.top
PING sni.github.map.fastly.net (151.101.77.147) 56(84) bytes of data.
64 bytes from 151.101.77.147: icmp_seq=1 ttl=44 time=409 ms
^C
--- sni.github.map.fastly.net ping statistics ---
2 packets transmitted, 1 received, 50% packet loss, time 1001ms
rtt min/avg/max/mdev = 409.978/409.978/409.978/0.000 ms

首先看一下有網(wǎng)絡(luò)連接的情況体谒,vimerzhao.github.iozhanglanqing.github.io是我和我同學(xué)的博客江兢,雖然內(nèi)容不一樣但是指向相同的IP实檀,協(xié)議、端口等都相同衬横,所以相等了裹粤;而vimerzhao.github.io雖然和vimerzhao.top指向同一個博客,但是一個是https一個是http蜂林,協(xié)議不同遥诉,所以判斷為不相等。相信這和大多數(shù)人的直覺是相背的:指向不同博客的URL相等了噪叙,但指向相同博客的URL卻不相等突那!
再分析斷網(wǎng)之后的結(jié)果:首先查看URL的源碼:


    public boolean equals(Object obj) {
        if (!(obj instanceof URL))
            return false;
        URL u2 = (URL)obj;

        return handler.equals(this, u2);
    }

再看handler對象的源碼:


    protected boolean equals(URL u1, URL u2) {
        String ref1 = u1.getRef();
        String ref2 = u2.getRef();
        return (ref1 == ref2 || (ref1 != null && ref1.equals(ref2))) &&
               sameFile(u1, u2);
    }

sameFile源碼:


    protected boolean sameFile(URL u1, URL u2) {
        // Compare the protocols.
        if (!((u1.getProtocol() == u2.getProtocol()) ||
              (u1.getProtocol() != null &&
               u1.getProtocol().equalsIgnoreCase(u2.getProtocol()))))
            return false;

        // Compare the files.
        if (!(u1.getFile() == u2.getFile() ||
              (u1.getFile() != null && u1.getFile().equals(u2.getFile()))))
            return false;

        // Compare the ports.
        int port1, port2;
        port1 = (u1.getPort() != -1) ? u1.getPort() : u1.handler.getDefaultPort();
        port2 = (u2.getPort() != -1) ? u2.getPort() : u2.handler.getDefaultPort();
        if (port1 != port2)
            return false;

        // Compare the hosts.
        if (!hostsEqual(u1, u2))
            return false;// 無網(wǎng)絡(luò)連接時會觸發(fā)這一句

        return true;
    }

最后是hostsEqual的源碼:


    protected boolean hostsEqual(URL u1, URL u2) {
        InetAddress a1 = getHostAddress(u1);
        InetAddress a2 = getHostAddress(u2);
        // if we have internet address for both, compare them
        if (a1 != null && a2 != null) {
            return a1.equals(a2);
        // else, if both have host names, compare them
        } else if (u1.getHost() != null && u2.getHost() != null)
            return u1.getHost().equalsIgnoreCase(u2.getHost());
         else
            return u1.getHost() == null && u2.getHost() == null;
    }

在有網(wǎng)絡(luò)的情況下,a1a2都不是null所以會觸發(fā)return a1.equals(a2)构眯,返回true;而沒有網(wǎng)絡(luò)時則會觸發(fā)return u1.getHost().equalsIgnoreCase(u2.getHost());即第二個判斷早龟,顯然url1hostvimerzhao.github.io)和url2hostzhanglanqing.github.io)不等惫霸,所以返回false猫缭,導(dǎo)致if (!hostsEqual(u1, u2))判斷為真,return false執(zhí)行壹店。
可見猜丹,URL類的equals方法不僅違反直覺還缺乏一致性,在不同環(huán)境會有不同結(jié)果硅卢,十分危險射窒!

耗時的equals方法

此外,equals還是個耗時的操作将塑,因?yàn)樵谟芯W(wǎng)絡(luò)的情況下需要進(jìn)行DNS解析脉顿,hashCode()同理,這里以hashCode()為例說明点寥。URL類的hashCode()源碼:

    public synchronized int hashCode() {
        if (hashCode != -1)
            return hashCode;

        hashCode = handler.hashCode(this);
        return hashCode;
    }

handler對象的hashCode()方法:


    protected int hashCode(URL u) {
        int h = 0;

        // Generate the protocol part.
        String protocol = u.getProtocol();
        if (protocol != null)
            h += protocol.hashCode();

        // Generate the host part.
        InetAddress addr = getHostAddress(u);
        if (addr != null) {
            h += addr.hashCode();
        } else {
            String host = u.getHost();
            if (host != null)
                h += host.toLowerCase().hashCode();
        }

        // Generate the file part.
        String file = u.getFile();
        if (file != null)
            h += file.hashCode();

        // Generate the port part.
        if (u.getPort() == -1)
            h += getDefaultPort();
        else
            h += u.getPort();

        // Generate the ref part.
        String ref = u.getRef();
        if (ref != null)
            h += ref.hashCode();

        return h;
    }

其中getHostAddress()會消耗大量時間艾疟。所以,如果在基于哈希表的容器中存儲URL對象敢辩,簡直就是災(zāi)難蔽莱。下面這段代碼,對比了URLURI在存儲50次時的表現(xiàn):

import java.net.*;
import java.util.*;

public class TestHash {
    public static void main(String args[]) {
        HashSet<URL> list1 = new HashSet<>();
        HashSet<URI> list2 = new HashSet<>();
        try {
            URL url1 = new URL("https://vimerzhao.github.io/");
            URI url2 = new URI("https://zhanglanqing.github.io/");
            long cur = System.currentTimeMillis();
            int cnt = 50;
            for (int i = 0; i < cnt; i++) {
                list1.add(url1);
            }
            System.out.println(System.currentTimeMillis() - cur);
            cur = System.currentTimeMillis();
            for (int i = 0; i < cnt; i++) {
                list2.add(url2);
            }
            System.out.println(System.currentTimeMillis() - cur);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

輸出為:

271
0

所以戚长,基于哈希表實(shí)現(xiàn)的容器最好不要用URL盗冷。

TrailingSlash的作用

所謂TrailingSlash就是域名結(jié)尾的斜杠。比如我們在瀏覽器看到vimerzhao.top,復(fù)制后粘貼發(fā)現(xiàn)是http://vimerzhao.top/同廉。首先用下面代碼測試:

import java.net.*;
import java.io.*;

public class TestTrailingSlash {
    public static void main(String args[]) {
        try {
            URL url1 = new URL("https://vimerzhao.github.io/");
            URL url2 = new URL("https://vimerzhao.github.io");
            System.out.println(url1.equals(url2));
            outputInfo(url1);
            outputInfo(url2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void outputInfo(URL url) {
        System.out.println("------" + url.toString() + "----------");
        System.out.println(url.getRef());
        System.out.println(url.getFile());
        System.out.println(url.getHost());
        System.out.println("----------------");
    }
}

得到結(jié)果如下:

false
------https://vimerzhao.github.io/----------
null
/
vimerzhao.github.io
----------------
------https://vimerzhao.github.io----------
null

vimerzhao.github.io
----------------

其實(shí)仪糖,無論用前面的read()方法讀或者地址欄直接輸入url,url1url2內(nèi)容都是相同的恤溶,但是加/表示這是一個目錄乓诽,不加表示這是一個文件,所以二者getFile()的結(jié)果不同咒程,導(dǎo)致equals判斷為false鸠天。在地址欄輸入時甚至不會覺察到這個TrailingSlash,所返回的結(jié)果也一樣帐姻,但equals判斷竟然為false稠集,真是防不勝防!
這里還有一個問題就是:一個是文件饥瓷,令一個是目錄剥纷,為什么都能得到相同結(jié)果?
調(diào)查一番后發(fā)現(xiàn):其實(shí)再請求的時候如果有/呢铆,那么就會在這個目錄下找index.html文件晦鞋;如果沒有,以vimerzhao.top/tags為例,則會先找tags悠垛,如果找不到就會自動在后面添加一個/线定,再在tags目錄下找index.html文件。如圖:

image

這里有一個有趣的測試确买,編寫兩段代碼如下:

import java.net.*;
import java.io.*;

public class TestTrailingSlash {
    public static void main(String args[]) {
        try {
            URL urlWithSlash = new URL("http://vimerzhao.top/tags/");
            int cnt = 5;
            long cur = System.currentTimeMillis();
            for (int i = 0; i < cnt; i++) {
                read(urlWithSlash);
            }
            System.out.println(System.currentTimeMillis() - cur);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void read(URL url) {
        try {
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(url.openStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                //System.out.println(inputLine);
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

import java.net.*;
import java.io.*;

public class TestWithoutTrailingSlash {
    public static void main(String args[]) {
        try {
            URL urlWithoutSlash = new URL("http://vimerzhao.top/tags");
            int cnt = 5;
            long cur = System.currentTimeMillis();
            for (int i = 0; i < cnt; i++) {
                read(urlWithoutSlash);
            }
            System.out.println(System.currentTimeMillis() - cur);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void read(URL url) {
        try {
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(url.openStream()));

            String inputLine;
            while ((inputLine = in.readLine()) != null) {
                //System.out.println(inputLine);
            }
            in.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用如下腳本測試:

#!/bin/sh
for i in {1..20}; do
    java TestTrailingSlash > out1
    java TestWithoutTrailingSlash > out2
done

將輸出的時間做成表格:


image

可以發(fā)現(xiàn)斤讥,添加了/的速度更快,這是因?yàn)?strong>省去了查找是否有tags文件的操作湾趾。這也給我們啟發(fā):URL結(jié)尾的/最好還是加上芭商!

以上,本周末發(fā)現(xiàn)的一些坑搀缠。

參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末铛楣,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子胡嘿,更是在濱河造成了極大的恐慌蛉艾,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,290評論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衷敌,死亡現(xiàn)場離奇詭異勿侯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)缴罗,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評論 2 385
  • 文/潘曉璐 我一進(jìn)店門助琐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人面氓,你說我怎么就攤上這事兵钮。” “怎么了舌界?”我有些...
    開封第一講書人閱讀 156,872評論 0 347
  • 文/不壞的土叔 我叫張陵掘譬,是天一觀的道長。 經(jīng)常有香客問我呻拌,道長葱轩,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,415評論 1 283
  • 正文 為了忘掉前任藐握,我火速辦了婚禮靴拱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘猾普。我一直安慰自己袜炕,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評論 6 385
  • 文/花漫 我一把揭開白布初家。 她就那樣靜靜地躺著偎窘,像睡著了一般乌助。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上评架,一...
    開封第一講書人閱讀 49,784評論 1 290
  • 那天眷茁,我揣著相機(jī)與錄音,去河邊找鬼纵诞。 笑死,一個胖子當(dāng)著我的面吹牛培遵,可吹牛的內(nèi)容都是我干的浙芙。 我是一名探鬼主播,決...
    沈念sama閱讀 38,927評論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼籽腕,長吁一口氣:“原來是場噩夢啊……” “哼嗡呼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起皇耗,我...
    開封第一講書人閱讀 37,691評論 0 266
  • 序言:老撾萬榮一對情侶失蹤南窗,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后郎楼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體万伤,經(jīng)...
    沈念sama閱讀 44,137評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評論 2 326
  • 正文 我和宋清朗相戀三年呜袁,在試婚紗的時候發(fā)現(xiàn)自己被綠了敌买。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,622評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡阶界,死狀恐怖虹钮,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情膘融,我是刑警寧澤芙粱,帶...
    沈念sama閱讀 34,289評論 4 329
  • 正文 年R本政府宣布,位于F島的核電站氧映,受9級特大地震影響春畔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜屯耸,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評論 3 312
  • 文/蒙蒙 一拐迁、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧疗绣,春花似錦线召、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,741評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽哈打。三九已至,卻和暖如春讯壶,著一層夾襖步出監(jiān)牢的瞬間料仗,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,977評論 1 265
  • 我被黑心中介騙來泰國打工伏蚊, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留立轧,地道東北人。 一個月前我還...
    沈念sama閱讀 46,316評論 2 360
  • 正文 我出身青樓躏吊,卻偏偏與公主長得像氛改,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子比伏,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評論 2 348

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理胜卤,服務(wù)發(fā)現(xiàn),斷路器赁项,智...
    卡卡羅2017閱讀 134,629評論 18 139
  • afinalAfinal是一個android的ioc葛躏,orm框架 https://github.com/yangf...
    passiontim閱讀 15,409評論 2 45
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,756評論 25 707
  • ---致烈日中在路上的農(nóng)資人 2016年農(nóng)產(chǎn)品價格持續(xù)低迷舰攒,農(nóng)資行業(yè)經(jīng)歷寒冬,大數(shù)據(jù)公布的農(nóng)化巨頭業(yè)績慘淡李剖,在這種...
    佑道閱讀 2,225評論 1 1
  • 曾經(jīng)為了一個人淡了身邊所有人芒率,可我從未后悔,因?yàn)橹辽傥矣X得他是愛我的篙顺,后來他當(dāng)著所有人的面與另一個人牽手偶芍,我才發(fā)現(xiàn)...
    手心的念閱讀 142評論 0 0