4. JSON字符串是如何被解析的?JsonParser瞭解一下

公司不是你家,領導不是你媽。本文已被 https://www.yourbatman.cn 收錄,裏面一併有Spring技術棧、MyBatis、JVM、中間件等小而美的 專欄供以避免費學習。關注公衆號【 BAT的烏托邦】逐個擊破,深刻掌握,拒絕淺嘗輒止。

前言

各位好,我是A哥(YourBatman)。上篇文章:3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON 聊完,流式API的寫部分能夠認爲你已徹底掌握了,本文了解它讀的部分。
java

版本約定

  • Jackson版本:2.11.0
  • Spring Framework版本:5.2.6.RELEASE
  • Spring Boot版本:2.3.0.RELEASE
小貼士:截止到本文,本系列 前面全部示例都只僅僅導入 jackson-core而已,後續若要新增jar包我會額外說明,不然相同

正文

什麼叫讀JSON?就是把一個JSON 字符串 解析爲對象or樹模型嘛,所以也稱做解析JSON串。Jackson底層流式API使用JsonParser來完成JSON字符串的解析。git

最簡使用Demo

準備一個POJO:github

@Data
public class Person {
    private String name;
    private Integer age;
}

測試用例:把一個JSON字符串綁定(封裝)進一個POJO對象裏json

@Test
public void test1() throws IOException {
    String jsonStr = "{\"name\":\"YourBatman\",\"age\":18}";
    Person person = new Person();

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        
        // 只要還沒結束"}",就一直讀
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                person.setName(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                person.setAge(jsonParser.getIntValue());
            }
        }
        
        System.out.println(person);
    }
}

運行程序,輸出:segmentfault

Person(name=YourBatman, age=18)

成功把一個JSON字符串的值解析到Person對象。你可能會疑問,怎麼這麼麻煩?那固然,這是底層流式API,純手動檔嘛。你得到了性能,可不要失去一些便捷性嘛。數組

小貼士:底層流式API通常面向「專業人士」,應用級開發使用高階API ObjectMapper便可。固然,讀完本系列就能讓你徹底具有「專業人士」的實力😄

JsonParser針對不一樣的value類型,提供了很是多的方法用於實際值的獲取。安全

直接值獲取:app

// 獲取字符串類型
public abstract String getText() throws IOException;

// 數字Number類型值 標量值(支持的Number類型參照NumberType枚舉)
public abstract Number getNumberValue() throws IOException;
public enum NumberType {
    INT, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL
};

public abstract int getIntValue() throws IOException;
public abstract long getLongValue() throws IOException;
...
public abstract byte[] getBinaryValue(Base64Variant bv) throws IOException;

這類方法可能會拋出異常:好比value值本不是數字但你調用了getInValue()方法~框架

小貼士:若是value值是null,像getIntValue()、getBooleanValue()等這種直接獲取方法是會拋出異常的,但getText()不會

帶默認值的值獲取,具備更好安全性:ide

public String getValueAsString() throws IOException {
    return getValueAsString(null);
}
public abstract String getValueAsString(String def) throws IOException;
...
public long getValueAsLong() throws IOException {
    return getValueAsLong(0);
}
public abstract long getValueAsLong(long def) throws IOException;
...

此類方法若碰到數據的轉換失敗時,不會拋出異常,把def做爲默認值返回。

組合方法

JsonGenerator同樣,JsonParser也提供了高鈣片組合方法,讓你更加便捷的使用。

自動綁定

聽起來像高級功能,是的,它必須依賴於ObjectCodec去實現,由於實際是所有委託給了它去完成的,也就是咱們最爲熟悉的readXXX系列方法:

咱們知道,ObjectMapper就是一個ObjectCodec,它屬於高級API,本文顯然不會用到ObjectMapper它嘍,所以咱們本身手敲一個實現來完成此功能。

自定義一個ObjectCodec,Person類專用:用於把JSON串自動綁定到實例屬性。

public class PersonObjectCodec extends ObjectCodec {
    ...
    @SneakyThrows
    @Override
    public <T> T readValue(JsonParser jsonParser, Class<T> valueType) throws IOException {
        Person person = (Person) valueType.newInstance();

        // 只要還沒結束"}",就一直讀
        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                person.setName(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                person.setAge(jsonParser.getIntValue());
            }
        }

        return (T) person;
    }
    ...
}

有了它,就能夠實現咱們的自動綁定了,書寫測試用例:

@Test
public void test3() throws IOException {
    String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        jsonParser.setCodec(new PersonObjectCodec());

        System.out.println(jsonParser.readValueAs(Person.class));
    }
}

運行程序,輸出:

Person(name=YourBatman, age=18)

這就是ObjectMapper自動綁定的核心原理所在,其它更爲強大能力將在後續章節詳細展開。

JsonToken

在上例解析過程當中,有一個很是重要的角色,那即是:JsonToken。它表示解析JSON內容時,用於返回結果的基本標記類型的枚舉。

public enum JsonToken {
    NOT_AVAILABLE(null, JsonTokenId.ID_NOT_AVAILABLE),
    
    START_OBJECT("{", JsonTokenId.ID_START_OBJECT),
    END_OBJECT("}", JsonTokenId.ID_END_OBJECT),
    START_ARRAY("[", JsonTokenId.ID_START_ARRAY),
    END_ARRAY("]", JsonTokenId.ID_END_ARRAY),

    // 屬性名(key)
    FIELD_NAME(null, JsonTokenId.ID_FIELD_NAME),

    // 值(value)
    VALUE_EMBEDDED_OBJECT(null, JsonTokenId.ID_EMBEDDED_OBJECT),
    VALUE_STRING(null, JsonTokenId.ID_STRING),
    VALUE_NUMBER_INT(null, JsonTokenId.ID_NUMBER_INT),
    VALUE_NUMBER_FLOAT(null, JsonTokenId.ID_NUMBER_FLOAT),
    VALUE_TRUE("true", JsonTokenId.ID_TRUE),
    VALUE_FALSE("false", JsonTokenId.ID_FALSE),
    VALUE_NULL("null", JsonTokenId.ID_NULL),
}

爲了輔助理解,A哥用一個例子,輸出各個部分一目瞭然:

@Test
public void test2() throws IOException {
    String jsonStr = "{\"name\":\"YourBatman\",\"age\":18, \"pickName\":null}";
    System.out.println(jsonStr);
    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {

        while (true) {
            JsonToken token = jsonParser.nextToken();
            System.out.println(token + " -> 值爲:" + jsonParser.getValueAsString());

            if (token == JsonToken.END_OBJECT) {
                break;
            }
        }
    }
}

運行程序,輸出:

{"name":"YourBatman","age":18, "pickName":null}
START_OBJECT -> 值爲:null


FIELD_NAME -> 值爲:name
VALUE_STRING -> 值爲:YourBatman

FIELD_NAME -> 值爲:age
VALUE_NUMBER_INT -> 值爲:18

FIELD_NAME -> 值爲:pickName
VALUE_NULL -> 值爲:null


END_OBJECT -> 值爲:null

從左至右解析,一一對應。各個部分用下面這張圖能夠簡略表示出來:

小貼士:解析時請確保你的的JSON串是合法的,不然拋出 JsonParseException異常

JsonParser的Feature

它是JsonParser的一個內部枚舉類,共15個枚舉值:

public enum Feature {
    AUTO_CLOSE_SOURCE(true),
    
    ALLOW_COMMENTS(false),
    ALLOW_YAML_COMMENTS(false),
    ALLOW_UNQUOTED_FIELD_NAMES(false),
    ALLOW_SINGLE_QUOTES(false),
    @Deprecated
    ALLOW_UNQUOTED_CONTROL_CHARS(false),
    @Deprecated
    ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false),
    @Deprecated
    ALLOW_NUMERIC_LEADING_ZEROS(false),
    @Deprecated
    ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false),
    @Deprecated
    ALLOW_NON_NUMERIC_NUMBERS(false),
    @Deprecated
    ALLOW_MISSING_VALUES(false),
    @Deprecated
    ALLOW_TRAILING_COMMA(false),
    
    STRICT_DUPLICATE_DETECTION(false),
    IGNORE_UNDEFINED(false),
    INCLUDE_SOURCE_IN_LOCATION(true);
}
小貼士:枚舉值均爲bool類型,括號內爲默認值

每一個枚舉值都控制着JsonParser不一樣的行爲。下面分類進行解釋

底層I/O流相關

自2.10版本後,使用 StreamReadFeature#AUTO_CLOSE_SOURCE代替

Jackson的流式API指的是I/O流,因此即便是,底層也是用I/O流(Reader)去讀取而後解析的。

AUTO_CLOSE_SOURCE(true)

原理和JsonGenerator的AUTO_CLOSE_TARGET(true)同樣,再也不解釋,詳見上篇文章對應部分。

支持非標準格式

JSON是有規範的,在它的規範裏並無描述到對註釋的規定、對控制字符的處理等等,也就是說這些均屬於非標準行爲。好比這個JSON串:

{
    "name" : "YourBarman", // 名字
    "age" : 18 // 年齡
}

你看,若你這麼寫IDEA都會飄紅提示你:

可是,在不少使用場景(特別是JavaScript)裏,咱們會在JSON串裏寫註釋(屬性多時尤甚)那麼對於這種串,JsonParser如何控制處理呢?它提供了對非標準JSON格式的兼容,經過下面這些特徵值來控制。

ALLOW_COMMENTS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_JAVA_COMMENTS代替

是否容許/* */或者//這種類型的註釋出現。

@Test
public void test4() throws IOException {
    String jsonStr = "{\n" +
            "\t\"name\" : \"YourBarman\", // 名字\n" +
            "\t\"age\" : 18 // 年齡\n" +
            "}";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // 開啓註釋支持
        // jsonParser.enable(JsonParser.Feature.ALLOW_COMMENTS);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getText());
            } else if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getIntValue());
            }
        }
    }
}

運行程序,拋出異常:

com.fasterxml.jackson.core.JsonParseException: Unexpected character ('/' (code 47)): maybe a (non-standard) comment? (not recognized as one since Feature 'ALLOW_COMMENTS' not enabled for parser)
 at [Source: (String)"{
    "name" : "YourBarman", // 名字
    "age" : 18 // 年齡
}"; line: 2, column: 26]

放開註釋的代碼,再次運行程序,正常work

ALLOW_YAML_COMMENTS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_YAML_COMMENTS代替

顧名思義,開啓後將支持Yaml格式的的註釋,也就是#形式的註釋語法。

ALLOW_UNQUOTED_FIELD_NAMES(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_UNQUOTED_FIELD_NAMES代替

是否容許屬性名不帶雙引號"",比較簡單,示例略。

ALLOW_SINGLE_QUOTES(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_SINGLE_QUOTES代替

是否容許屬性名支持單引號,也就是使用''包裹,形如這樣:

{
    'age' : 18
}
ALLOW_UNQUOTED_CONTROL_CHARS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_UNESCAPED_CONTROL_CHARS代替

是否容許JSON字符串包含非引號控制字符(值小於32的ASCII字符,包含製表符和換行符)。 因爲JSON規範要求對全部控制字符使用引號,這是一個非標準的特性,所以默認禁用。

那麼,哪些字符屬於控制字符呢?作個簡單科普:咱們通常說的ASCII碼共128個字符(7bit),共分爲兩大類

控制字符

控制字符,也叫不可打印字符。第0~32號及第127號(共34個)是控制字符,例如常見的:LF(換行)CR(回車)、FF(換頁)、DEL(刪除)、BS(退格)等都屬於此類。

控制字符大部分已經廢棄不用了,它們的用途主要是用來操控已經處理過的文字,ASCII值爲八、九、10 和13 分別轉換爲退格、製表、換行和回車字符。它們並無特定的圖形顯示,但會依不一樣的應用程序,而對文本顯示有不一樣的影響。

話外音:你看不見我,但我對你影響還蠻大
非控制字符

也叫可顯示字符,或者可打印字符,能從鍵盤直接輸入的字符。好比0-9數字,逗號、分號這些等等。

話外音:你肉眼能看到的字符就屬於非控制字符
ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER代替

是否容許*反斜槓*轉義任何字符。這句話不是很是好理解,看下面這個例子:

@Test
public void test4() throws IOException {
    String jsonStr = "{\"name\" : \"YourB\\'atman\" }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_BACKSLASH_ESCAPING_ANY_CHARACTER);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("name".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getText());
            }
        }
    }
}

運行程序,報錯:

com.fasterxml.jackson.core.JsonParseException: Unrecognized character escape ''' (code 39)
 at [Source: (String)"{"name" : "YourB\'atman" }"; line: 1, column: 19]
 ...

放開註釋掉的代碼,再次運行程序,一切正常,輸出:YourB'atman

ALLOW_NUMERIC_LEADING_ZEROS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_LEADING_ZEROS_FOR_NUMBERS代替

是否容許像00001這樣的「數字」出現(而不報錯)。看例子:

@Test
public void test5() throws IOException {
    String jsonStr = "{\"age\" : 00018 }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getIntValue());
            }
        }
    }
}

運行程序,輸出:

com.fasterxml.jackson.core.JsonParseException: Invalid numeric value: Leading zeroes not allowed
 at [Source: (String)"{"age" : 00018 }"; line: 1, column: 11]
 ...

放開注掉的代碼,再次運行程序,一切正常。輸出18

ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_LEADING_DECIMAL_POINT_FOR_NUMBERS代替

是否容許小數點.打頭,也就是說.1這種小數格式是否合法。默認是不合法的,須要開啓此特徵才能支持,例子就略了,基本同上。

ALLOW_NON_NUMERIC_NUMBERS(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_NON_NUMERIC_NUMBERS代替

是否容許一些解析器識別一組「非數字」(如NaN)做爲合法的浮點數值。這個屬性和上篇文章的JsonGenerator#QUOTE_NON_NUMERIC_NUMBERS特徵值是遙相呼應的。

@Test
public void test5() throws IOException {
    String jsonStr = "{\"percent\" : NaN }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("percent".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getFloatValue());
            }
        }
    }
}

運行程序,拋錯:

com.fasterxml.jackson.core.JsonParseException: Non-standard token 'NaN': enable JsonParser.Feature.ALLOW_NON_NUMERIC_NUMBERS to allow
 at [Source: (String)"{"percent" : NaN }"; line: 1, column: 17]

放開註釋掉的代碼,再次運行,一切正常。輸出:

NaN
小貼士:NaN也能夠表示一個Float對象,是的你沒聽錯,即便它不是 數字但它也是Float類型。具體你能夠看看Float源碼裏的那幾個常量
ALLOW_MISSING_VALUES(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_MISSING_VALUES代替

是否容許支持JSON數組中「缺失」值。怎麼理解:數組中缺失了值表示兩個逗號之間,啥都沒有,形如這樣[value1, , value3]

@Test
public void test6() throws IOException {
    String jsonStr = "{\"names\" : [\"YourBatman\",,\"A哥\",,] }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("names".equals(fieldname)) {
                jsonParser.nextToken();

                while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                    System.out.println(jsonParser.getText());
                }
            }
        }
    }
}

運行程序,拋錯:

YourBatman // 能輸出一個,畢竟第一個part(JsonToken)是正常的嘛

com.fasterxml.jackson.core.JsonParseException: Unexpected character (',' (code 44)): expected a valid value (JSON String, Number, Array, Object or token 'null', 'true' or 'false')
 at [Source: (String)"{"names" : ["YourBatman",,"A哥",,] }"; line: 1, column: 27]

放開註釋掉的代碼,再次運行,一切正常,結果爲:

YourBatman
null
A哥
null
null

請注意:此時數組的長度是5哦。

小貼士:此處用的String類型展現結果,是由於null能夠做爲String類型( jsonParser.getText()獲得null是合法的)。但若是你使用的int類型(或者bool類型),那麼若是是null的話就報錯嘍 Current token (VALUE_NULL) not of boolean type,有興趣的親可自行嘗試,鞏固下理解的效果。報錯緣由文上已有說明~
ALLOW_TRAILING_COMMA(false)
自2.10版本後,使用 JsonReadFeature#ALLOW_TRAILING_COMMA代替

是否容許最後一個多餘的逗號(必定是最後一個)。這個特徵是很是重要的,若開關打開,有以下效果:

  • [true,true,]等價於[true, true]
  • {"a": true,}等價於{"a": true}

當這個特徵和上面的ALLOW_MISSING_VALUES特徵同時使用時,本特徵優先級更高。也就是說:會先去除掉最後一個逗號後,再進行數組長度的計算。

舉個例子:固然這兩個特徵開關都打開時,[true,true,]等價於[true, true]好理解;而且呢,[true,true,,]是等價於[true, true, null]的哦,可千萬別忽略最後的這個null

@Test
public void test7() throws IOException {
    String jsonStr = "{\"results\" : [true,true,,] }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        jsonParser.enable(JsonParser.Feature.ALLOW_MISSING_VALUES);
        // jsonParser.enable(JsonParser.Feature.ALLOW_TRAILING_COMMA);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("results".equals(fieldname)) {
                jsonParser.nextToken();

                while (jsonParser.nextToken() != JsonToken.END_ARRAY) {
                    System.out.println(jsonParser.getBooleanValue());
                }
            }
        }
    }
}

運行程序,輸出:

YourBatman
null
A哥
null
null

這徹底就是上例的效果嘛。如今我放開註釋掉的代碼,再次運行,結果爲:

YourBatman
null
A哥
null

請注意對比先後的結果差別,並本身能能本身合理解釋

校驗相關

Jackson在JSON標準以外,給出了兩個校驗相關的特徵。

STRICT_DUPLICATE_DETECTION(false)
自2.10版本後,使用 StreamReadFeature#STRICT_DUPLICATE_DETECTION代替

是否容許JSON串有兩個相同的屬性key,默認是容許的

@Test
public void test8() throws IOException {
    String jsonStr = "{\"age\":18, \"age\": 28 }";

    JsonFactory factory = new JsonFactory();
    try (JsonParser jsonParser = factory.createParser(jsonStr)) {
        // jsonParser.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION);

        while (jsonParser.nextToken() != JsonToken.END_OBJECT) {
            String fieldname = jsonParser.getCurrentName();
            if ("age".equals(fieldname)) {
                jsonParser.nextToken();
                System.out.println(jsonParser.getIntValue());
            }
        }
    }
}

運行程序,正常輸出:

18
28

若放開註釋代碼,再次運行,則拋錯:

18 // 第一個數字仍是能正常輸出的喲

com.fasterxml.jackson.core.JsonParseException: Duplicate field 'age'
 at [Source: (String)"{"age":18, "age": 28 }"; line: 1, column: 17]
IGNORE_UNDEFINED(false)
自2.10版本後,使用 StreamReadFeature#IGNORE_UNDEFINED代替

是否忽略沒有定義的屬性key。和JsonGenerator.Feature#IGNORE_UNKNOWN的這個特徵同樣,它做用於預先定義了格式的數據類型,如Avro、protobuf等等,JSON是不須要預先定義的哦~

一樣的,你能夠經過這個API預先設置格式:

JsonParser:

    public void setSchema(FormatSchema schema) {
        ...
    }

其它

INCLUDE_SOURCE_IN_LOCATION(true)
自2.10版本後,使用 StreamReadFeature#INCLUDE_SOURCE_IN_LOCATION代替

是否構建JsonLocation對象來表示每一個part的來源,你能夠經過JsonParser#getCurrentLocation()來訪問。做用不大,就此略過。

總結

本文介紹了底層流式API JsonParser讀JSON的方式,它不只僅可以處理標準JSON,也能經過Feature特徵值來控制,開啓對一些非標準但又比較經常使用的JSON串的支持,這不正式一個優秀框架/庫應有的態度麼:兼容性

結合上篇文章對寫JSON時JsonGenerator的描述,可以總結出兩點原則:

  • 寫:100%遵循規範
  • 讀:最大程度兼容幷包

寫表明你的輸出,遵循規範的輸出能確保第三方在用你輸出的數據時不至於對你破口大罵,因此這是你應該作好的本分。讀表明你的輸入,可以處理規範的格式是你的職責,但我若還能額外的處理一些非標準格式(通常爲經常使用的),那絕對是閃耀點,也就是你給的情分。本分是你應該作的,而情分就是你的加分項。

相關推薦:

關注A哥

Author A哥(YourBatman)
我的站點 www.yourbatman.cn
E-mail yourbatman@qq.com
微 信 fsx641385712
活躍平臺
公衆號 BAT的烏托邦(ID:BAT-utopia)
知識星球 BAT的烏托邦
每日文章推薦 每日文章推薦

BAT的烏托邦

相關文章
相關標籤/搜索