直擊痛點(diǎn)的一款 HTTP 客戶端框架!

大家好户辫,我是二哥呀渔欢!

今天來給大家推薦一款直擊痛點(diǎn)的 HTTP 客戶端框架苫幢,可以超高效率地完成和第三方接口的對(duì)接韩肝。

在介紹本篇的主角之前哀峻,我們先來了解下 Java 生態(tài)中的 HTTP 組件庫剩蟀,大致可以分為三類:

  • JDK 自帶的 HttpURLConnection 標(biāo)準(zhǔn)庫育特;
  • Apache HttpComponents HttpClient且预;
  • OkHttp。

使用 HttpURLConnection 發(fā)起 HTTP 請(qǐng)求最大的優(yōu)點(diǎn)是不需要引入額外的依賴涮拗,但是使用起來非常繁瑣三热,也缺乏連接池管理、域名機(jī)械控制等特性支持抑堡。

使用標(biāo)準(zhǔn)庫的最大好處就是不需要引入額外的依賴首妖,但使用起來比較繁瑣,就像直接使用 JDBC 連接數(shù)據(jù)庫那樣,需要很多模板代碼棚壁。來發(fā)起一個(gè)簡(jiǎn)單的 HTTP POST 請(qǐng)求吧杯矩。

public class HttpUrlConnectionDemo {
    public static void main(String[] args) throws IOException {
        String urlString = "https://httpbin.org/post";
        String bodyString = "password=123";

        URL url = new URL(urlString);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setDoOutput(true);

        OutputStream os = conn.getOutputStream();
        os.write(bodyString.getBytes("utf-8"));
        os.flush();
        os.close();

        if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) {
            InputStream is = conn.getInputStream();
            BufferedReader reader = new BufferedReader(new InputStreamReader(is));
            StringBuilder sb = new StringBuilder();
            String line;
            while ((line = reader.readLine()) != null) {
                sb.append(line);
            }
            System.out.println("響應(yīng)內(nèi)容:" + sb.toString());
        } else {
            System.out.println("響應(yīng)碼:" + conn.getResponseCode());
        }
    }
}

HttpURLConnection 發(fā)起的 HTTP 請(qǐng)求比較原始,基本上算是對(duì)網(wǎng)絡(luò)傳輸層的一次淺層次的封裝灌曙;有了 HttpURLConnection 對(duì)象后菊碟,就可以獲取到輸出流,然后把要發(fā)送的內(nèi)容發(fā)送出去在刺;再通過輸入流讀取到服務(wù)器端響應(yīng)的內(nèi)容逆害;最后打印。

不過 HttpURLConnection 不支持 HTTP/2.0蚣驼,為了解決這個(gè)問題纯陨,Java 9 的時(shí)候官方的標(biāo)準(zhǔn)庫增加了一個(gè)更高級(jí)別的 HttpClient阴颖,再發(fā)起 POST 請(qǐng)求就顯得高大上多了偎肃,不僅支持異步,還支持順滑的鏈?zhǔn)秸{(diào)用稀火。

public class HttpClientDemo {
    public static void main(String[] args) throws URISyntaxException {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder()
                .uri(new URI("https://postman-echo.com/post"))
                .headers("Content-Type", "text/plain;charset=UTF-8")
                .POST(HttpRequest.BodyPublishers.ofString("二哥牛逼"))
                .build();
        client.sendAsync(request, HttpResponse.BodyHandlers.ofString())
                .thenApply(HttpResponse::body)
                .thenAccept(System.out::println)
                .join();
    }
}

Apache HttpComponents HttpClient 支持的特性也非常豐富:

  • 基于標(biāo)準(zhǔn)团甲、純凈的Java語言,實(shí)現(xiàn)了HTTP1.0和HTTP1.1牛隅;
  • 以可擴(kuò)展的面向?qū)ο蟮慕Y(jié)構(gòu)實(shí)現(xiàn)了HTTP全部的方法丈攒;
  • 支持加密的HTTPS協(xié)議(HTTP通過SSL協(xié)議)显设;
  • Request的輸出流可以避免流中內(nèi)容體直接從socket緩沖到服務(wù)器慷妙;
  • Response的輸入流可以有效的從socket服務(wù)器直接讀取相應(yīng)內(nèi)容狞山。
public class HttpComponentsDemo {
    public static void main(String[] args) throws IOException, IOException, ParseException {
        try (CloseableHttpClient httpclient = HttpClients.createDefault()) {
            HttpPost httpPost = new HttpPost("http://httpbin.org/post");
            List<NameValuePair> nvps = new ArrayList<>();
            nvps.add(new BasicNameValuePair("name", "二哥"));
            httpPost.setEntity(new UrlEncodedFormEntity(nvps, Charset.forName("UTF-8")));

            try (CloseableHttpResponse response2 = httpclient.execute(httpPost)) {
                System.out.println(response2.getCode() + " " + EntityUtils.toString(response2.getEntity()));
            }
        }
    }
}

OkHttp 是一個(gè)執(zhí)行效率比較高的 HTTP 客戶端:

  • 支持 HTTP/2.0屡律,當(dāng)多個(gè)請(qǐng)求對(duì)應(yīng)同一個(gè) Host 地址時(shí),可共用同一個(gè) Socket穿挨;
  • 連接池可減少請(qǐng)求延遲厉萝;
  • 支持 GZIP 壓縮彩郊,減少網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)大小惠险;
  • 支持 Response 數(shù)據(jù)緩存强经,避免重復(fù)網(wǎng)絡(luò)請(qǐng)求鲸伴;
public class OkHttpPostDemo {
    public static final MediaType JSON = MediaType.get("application/json; charset=utf-8");

    OkHttpClient client = new OkHttpClient();

    String post(String url, String json) throws IOException {
        RequestBody body = RequestBody.create(json, JSON);
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        try (Response response = client.newCall(request).execute()) {
            return response.body().string();
        }
    }

    public static void main(String[] args) throws IOException {
        OkHttpPostDemo example = new OkHttpPostDemo();
        String json = "{'name':'二哥'}";
        String response = example.post("https://httpbin.org/post", json);
        System.out.println(response);
    }
}

那今天介紹的這款輕量級(jí)的 HTTP 客戶端框架 Forest毕谴,正是基于 Httpclient和OkHttp 的,屏蔽了不同細(xì)節(jié)的 HTTP 組件庫所帶來的所有差異。

Forest 的字面意思是森林的意思,更內(nèi)涵點(diǎn)的話,可以拆成For和Rest兩個(gè)單詞站超,也就是“為了Rest”(Rest為一種基于HTTP的架構(gòu)風(fēng)格)。 而合起來就是森林,森林由很多樹木花草組成(可以理解為各種不同的服務(wù)),它們表面上看獨(dú)立耙箍,實(shí)則在地下根莖交錯(cuò)縱橫、相互連接依存猾骡,這樣看就有點(diǎn)現(xiàn)代分布式服務(wù)化的味道了曼振。 最后脐瑰,這兩個(gè)單詞反過來讀就像是Resultful妖枚。

項(xiàng)目地址:

https://gitee.com/dromara/forest

雖然 star 數(shù)還不是很多,但 star 趨勢(shì)圖正在趨于爬坡階段苍在,大家可以拿來作為一個(gè)練手項(xiàng)目绝页,我覺得還是不錯(cuò)的選擇

Forest 本身是處理前端過程的框架寂恬,是對(duì)后端 HTTP API 框架的進(jìn)一步封裝续誉。

前端部分:

  • 通過RPC方式去發(fā)送HTTP請(qǐng)求, 方便解耦
  • 支持GET, HEAD, POST等所有請(qǐng)求方法
  • 支持Spring和Springboot集成
  • JSON字符串到Java對(duì)象的自動(dòng)化解析
  • XML文本到Java對(duì)象的自動(dòng)化解析
  • 支持靈活的模板表達(dá)式
  • 支持?jǐn)r截器處理請(qǐng)求的各個(gè)生命周期
  • 支持自定義注解

后端部分:

  • 支持OkHttp
  • 支持Httpclient

Forest 容易上手,不需要調(diào)用HTTP底層接口初肉,而是像 Dubbo 那樣的 RPC 框架一樣酷鸦,只需要定義接口、調(diào)用接口即可牙咏。幾分鐘內(nèi)就可完成請(qǐng)求的定義井佑、發(fā)送、接收響應(yīng)眠寿、數(shù)據(jù)解析躬翁、錯(cuò)誤處理、日志打印等過程盯拱。

配置輕量盒发,遵循約定優(yōu)于配置的原則,只需在需要的時(shí)候進(jìn)行配置狡逢,不配置也不會(huì)影響Forest請(qǐng)求的正常調(diào)用宁舰。

簡(jiǎn)單優(yōu)雅,將 HTTP 請(qǐng)求細(xì)節(jié)封裝成 Java 接口 + 注解的形式奢浑,不必再關(guān)心發(fā)送 HTTP 請(qǐng)求的具體過程蛮艰。使得 HTTP 請(qǐng)求信息與業(yè)務(wù)代碼解耦,方便管理大量 HTTP 的 URL雀彼、Header壤蚜、Body 等信息即寡。

擴(kuò)展靈活,允許自定義攔截器袜刷、甚至是自定義注解聪富,以此來擴(kuò)展Forest的能力。

Forest 不需要我們編寫具體的 HTTP 調(diào)用過程著蟹,只需要定義一個(gè)接口墩蔓,然后通過 Forest 注解將 HTTP 請(qǐng)求的信息添加到接口的方法上即可。請(qǐng)求發(fā)送方通過調(diào)用定義的接口就能自動(dòng)發(fā)送請(qǐng)求和接受請(qǐng)求的響應(yīng)萧豆。

Forest 之所以能做到這樣奸披,是因?yàn)樗鼘⒍x好的接口通過動(dòng)態(tài)代理的方式生成了一個(gè)具體的實(shí)現(xiàn)類,然后組織涮雷、驗(yàn)證 HTTP 請(qǐng)求信息源内,綁定動(dòng)態(tài)數(shù)據(jù),轉(zhuǎn)換數(shù)據(jù)形式份殿,SSL 驗(yàn)證簽名,調(diào)用后端 HTTP API執(zhí)行實(shí)際請(qǐng)求嗽交,等待響應(yīng)卿嘲,失敗重試,轉(zhuǎn)換響應(yīng)數(shù)據(jù)到 Java 類型等臟活累活都由這動(dòng)態(tài)代理的實(shí)現(xiàn)類給包了夫壁。

廢話就不再多說拾枣,直接開始實(shí)戰(zhàn)。

第一步盒让,添加 Maven 依賴梅肤。

<dependency>
    <groupId>com.dtflys.forest</groupId>
    <artifactId>forest-core</artifactId>
    <version>1.5.1</version>
</dependency>

第二步,構(gòu)建 HTTP 請(qǐng)求邑茄。

在 Forest 中姨蝴,所有的 HTTP 請(qǐng)求信息都要綁定到某一個(gè)接口的方法上,不需要編寫具體的代碼去發(fā)送請(qǐng)求肺缕。請(qǐng)求發(fā)送方通過調(diào)用事先定義好 HTTP 請(qǐng)求信息的接口方法左医。

public interface ForRestClient {
    @Request(
            url = "http://httpbin.org/post",
            type = "POST"
    )
    String simplePost(@Body("name") String name);
}

通過 @Post 注解,將上面的ForRestClient接口中的 simplePost() 方法綁定了一個(gè) HTTP 請(qǐng)求同木,使用 POST 方式浮梢,可以使用@Body注解修飾參數(shù)的方式,將傳入?yún)?shù)的數(shù)據(jù)綁定到 HTTP 請(qǐng)求體中彤路。然后將請(qǐng)求響應(yīng)的數(shù)據(jù)以String的方式返回給調(diào)用者秕硝。

第三步,調(diào)用接口洲尊。

public class ForRestDemo {
    public static void main(String[] args) {
        // 實(shí)例化Forest配置對(duì)象
        ForestConfiguration configuration = ForestConfiguration.configuration();
        configuration.setBackendName("httpclient");

        // 通過Forest配置對(duì)象實(shí)例化Forest請(qǐng)求接口
        ForRestClient myClient = configuration.createInstance(ForRestClient.class);

        // 調(diào)用Forest請(qǐng)求接口远豺,并獲取響應(yīng)返回結(jié)果
        String result = myClient.simplePost("二哥");
        System.out.println(result);
    }
}

ForestConfiguration為 Forest 的全局配置對(duì)象類奈偏,所有的 Forest 的全局基本配置信息由此類進(jìn)行管理。

可以來看一下運(yùn)行后的日志信息:

{
  "args": {}, 
  "data": "", 
  "files": {}, 
  "form": {
    "name": "\u4e8c\u54e5"
  }, 
  "headers": {
    "Content-Length": "23", 
    "Content-Type": "application/x-www-form-urlencoded", 
    "Host": "httpbin.org", 
    "User-Agent": "Apache-HttpClient/4.5.2 (Java/11.0.8)", 
    "X-Amzn-Trace-Id": "Root=1-60b533aa-58b41e4967803d99593c53a0"
  }, 
  "json": null, 
  "origin": "161.81.21.32", 
  "url": "http://httpbin.org/post"
}

此時(shí)憋飞,一個(gè)簡(jiǎn)單的 Forest 上手小栗子就跑通了霎苗。

如果是 Spring Boot 項(xiàng)目的話,就不需要 ForestConfiguration 了榛做,只需要在啟動(dòng)類或者配置類上添加 @ForestScan 注解就可以了唁盏。

@SpringBootApplication
@Configuration
@ForestScan(basePackages = "com.yoursite.client")
public class MyApp {
 ...
}

Forest 除了支持GET和POST,也支持其他幾種 HTTP 請(qǐng)求方式检眯,比如PUT厘擂、HEAD、 OPTIONS锰瘸、DELETE刽严。只需要在構(gòu)建接口的時(shí)候使用對(duì)應(yīng)的注解就可以了,比如說 PUT:

// PUT請(qǐng)求
@Put("http://localhost:8080/hello")
String simplePut();

在POST和PUT請(qǐng)求方法中避凝,通常使用 HTTP 請(qǐng)求體進(jìn)行數(shù)據(jù)傳輸舞萄,在 Forest 中,可以使用 @Body管削、@JSONBody倒脓、@XMLBody 等多種方式設(shè)置請(qǐng)求體數(shù)據(jù)。

/**
 * 直接修飾一個(gè)JSON字符串
 */
@Post("http://localhost:8080/hello/user")
String helloUser(@JSONBody String userJson);

Forest 請(qǐng)求會(huì)自動(dòng)將響應(yīng)的返回?cái)?shù)據(jù)反序列化成對(duì)應(yīng)的數(shù)據(jù)類型含思,分兩步走崎弃。

第一步:定義dataType屬性

dataType屬性指定了該請(qǐng)求響應(yīng)返回的數(shù)據(jù)類型,可選的數(shù)據(jù)類型有三種: text, json, xml含潘,默認(rèn)為 text饲做。

/**
 * dataType為json或xml時(shí),F(xiàn)orest會(huì)進(jìn)行相應(yīng)的反序列化
 */
@Request(
    url = "http://localhost:8080/text/data",
    dataType = "json"
)
Map getData();

第二步:指定反序列化的目標(biāo)類型

反序列化需要一個(gè)目標(biāo)類型遏弱,而該類型其實(shí)就是方法的返回值類型盆均,如返回值為String就會(huì)反序列成String字符串,返回值為Map就會(huì)反序列化成一個(gè)HashMap對(duì)象漱逸,也可以指定為自定義的Class類型缀踪。

如果有這樣一個(gè) User 類:

public class User {
    private String username;
    private String score;
    
    // Setter和Getter ...
}

返回的數(shù)據(jù)為 JSON 字符串:

{"username":  "Foo", "score":  "82"}

那請(qǐng)求接口就應(yīng)該定義成這樣:

/**
 * dataType屬性指明了返回的數(shù)據(jù)類型為JSON
 */
@Get(
    url = "http://localhost:8080/user?id=${0}",
    dataType = "json"
)
User getUser(Integer id)

另外,大家需要了解一下 Gzip虹脯,它是現(xiàn)在一種流行的文件壓縮算法驴娃,有相當(dāng)廣泛的應(yīng)用范圍。尤其是當(dāng)Gzip用來壓縮存文本文件的時(shí)候效果尤為明顯循集,大概能減少70%以上的文件大小唇敞。很多 HTTP 服務(wù)器都支持 Gzip,比如 Tomcat,經(jīng)過這些服務(wù)壓縮過的數(shù)據(jù)可以降低網(wǎng)絡(luò)傳輸?shù)牧髁拷幔岣呖蛻舳说捻憫?yīng)速度咒精。

Forest從1.5.2-BETA版本開始支持Gzip的解壓,其解壓的方式也很簡(jiǎn)單旷档,在方法或接口類上加上 @DecompressGzip 注解即可模叙。

/**
 * 為請(qǐng)求方法添加Gzip解壓能力
 */
@Get("/transaction")
@DecompressGzip
String transaction(String infno);

更重要的一點(diǎn)是,F(xiàn)orest 可以通過設(shè)置@Request注解的async屬性為true來實(shí)現(xiàn)異步請(qǐng)求鞋屈。

@Request(
        url = "http://localhost:8080/hello/user?username=${0}",
        async = true,
        headers = {"Accept:text/plain"}
)
void asyncGet(String username范咨, OnSuccess<String> onSuccess);

異步請(qǐng)求時(shí),通過 OnSuccess<T> 回調(diào)函數(shù)來接受響應(yīng)數(shù)據(jù)厂庇,而不是通過接口方法的返回值渠啊,所以這里的返回值類型一般會(huì)定義為void。

調(diào)用該接口方法時(shí)权旷,可以通過下面的方式:

myClient.send("foo", (String resText, ForestRequest request, ForestResponse response) -> {
        // 成功響應(yīng)回調(diào)
        System.out.println(resText);    
    },
    (ForestRuntimeException ex, ForestRequest request, ForestResponse response) -> {
        // 異程骝龋回調(diào)
        System.out.println(ex.getMessage());
    });

除了上面提到的這些功能,F(xiàn)orset 還支持更高級(jí)的用法:

  • HTTPS
  • 文件上傳下載
  • 攔截器
  • 使用代理
  • 自定義注解

大家可以去看一下 Forset 的官方文檔拄氯,然后在本地實(shí)踐一下躲查,還是能學(xué)到不少知識(shí)的,尤其是 HTTPS 和文件上傳下載這塊译柏,只需要簡(jiǎn)單的配置就能完成镣煮,我個(gè)人感覺還是挺值得去學(xué)習(xí)和借鑒的。

開源精神難能可貴艇纺,好的開源需要大家的添磚加瓦和支持。希望這篇文章能給大家在選擇 HTTP 客戶端框架時(shí)帶來一個(gè)新的選擇邮弹,對(duì)黔衡,就是 Forest。

這篇文章不僅介紹了 Forest 這個(gè)輕量級(jí)的 HTTP 客戶端框架腌乡,還回顧了它的底層實(shí)現(xiàn):HttpClient 和 OkHttp盟劫,希望能對(duì)大家有所幫助。

我是二哥呀与纽,我們下期見~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末侣签,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子急迂,更是在濱河造成了極大的恐慌影所,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件僚碎,死亡現(xiàn)場(chǎng)離奇詭異猴娩,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門卷中,熙熙樓的掌柜王于貴愁眉苦臉地迎上來矛双,“玉大人,你說我怎么就攤上這事蟆豫∫楹觯” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵十减,是天一觀的道長(zhǎng)栈幸。 經(jīng)常有香客問我,道長(zhǎng)嫉称,這世上最難降的妖魔是什么侦镇? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮织阅,結(jié)果婚禮上壳繁,老公的妹妹穿的比我還像新娘。我一直安慰自己荔棉,他們只是感情好闹炉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著润樱,像睡著了一般渣触。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上壹若,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天嗅钻,我揣著相機(jī)與錄音,去河邊找鬼店展。 笑死养篓,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的赂蕴。 我是一名探鬼主播柳弄,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼概说!你這毒婦竟也來了碧注?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤糖赔,失蹤者是張志新(化名)和其女友劉穎萍丐,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體放典,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碉纺,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年船万,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片骨田。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡耿导,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出态贤,到底是詐尸還是另有隱情舱呻,我是刑警寧澤,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布悠汽,位于F島的核電站箱吕,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏柿冲。R本人自食惡果不足惜茬高,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望假抄。 院中可真熱鬧怎栽,春花似錦、人聲如沸宿饱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽谬以。三九已至强饮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間为黎,已是汗流浹背邮丰。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铭乾,地道東北人剪廉。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像片橡,于是被迫代替她去往敵國(guó)和親妈经。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淮野,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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