復習盤點-Java序列化方式(2)JAVA原生序列化以及Protostuff序列化

Java中的RPC(遠程服務調用)可以通過Serializable的方式進行具帮。

序(序列化和反序列化)

是什么?為啥用?怎么用蜂厅?——靈魂三連

  1. 序列化和反序列化是什么匪凡?

    • 序列化:把對象轉變?yōu)?code>字節(jié)序列的過程稱為對象的序列化。

    • 反序列化:把字節(jié)序列恢復為對象的過程稱為對象的反序列化掘猿。

  2. 對象序列化的用途

    • 將內存中對象的字節(jié)持久化到硬盤中的時候病游;
    • 當使用Socket在網絡上傳輸對象的時候;
    • 當使用RMI(遠程方法調用)傳輸對象的時候稠通;

1. Serializable序列化

類的序列化是實現(xiàn)java.io.Serializable接口啟動的衬衬,不實現(xiàn)此接口的類將不會有任何狀態(tài)的序列化和反序列化。序列化接口沒有方法或字段改橘,僅用于標識序列化的語義滋尉。

1.1 Serializable序列化的注意事項

1.1.1 序列化ID問題

Intellij IDEA生成serialVersionUID

虛擬機是否允許反序列化,不僅取決于類路徑和功能代碼是否一致飞主,還取決于兩個類序列化ID是否一致(ANY-ACCESS-MODIFIER static final long serialVersionUID = 42L;)兼砖。

如果可序列化類沒有顯示聲明SerialVersionUID,則序列化運行時將根據(jù)Java對象序列化規(guī)范中所述的類的各方面計算該類的默認SerialVersionUID既棺。但是強烈建議所有可序列化的類都明確聲明serialVersionUID值。因為默認得UID計算對類詳細信息非常敏感懒叛,這可能因編譯器實現(xiàn)而異丸冕,可能會導致反序列化InvalidClassException

序列化和反序列化代碼詳見JAVA BIO體系——ObjectInputStream/ObjectOutputStream對象流的使用

1. 反序列化不同的類路徑導致ClassCastException異常

Exception in thread "main" java.lang.ClassCastException: 
com.JsonSerializer.User cannot be cast to com.IODemo.BIODemo.User
    at com.IODemo.BIODemo.ObjectOut.main(ObjectOut.java:12)

2. 反序列化不同的UID導致InvalidClassException異常

java.io.InvalidClassException: com.JsonSerializer.User; local class incompatible:
 stream classdesc serialVersionUID = 4731277808546534921,
 local class serialVersionUID = 4731277808546534920

序列化ID一般有兩種生成規(guī)則薛窥,一種是固定的1L胖烛,一種是隨機生成一個不重復long類型數(shù)據(jù)。

  • 如果是沒有特殊需求诅迷,就用默認的1L就可以佩番,這樣就可以確保代碼一致時反序列化成功;

  • 隨機生成的序列化ID有什么用呢罢杉?有些時候趟畏,通過改變序列ID可以用來限制某些用戶的使用;

1.1.2 特殊變量序列化

1. 靜態(tài)變量的序列化
序列化并不保存靜態(tài)變量滩租,序列化保存的是對象的狀態(tài)赋秀,而靜態(tài)變量是的狀態(tài)。
2. Transient關鍵字
transient[?tr?nzi?nt]臨時態(tài))關鍵字的作用就是控制變量的序列化律想,在變量聲明前加上該關鍵字猎莲,可以阻止該變量序列化到文件中,在反序列化后技即,transient變量會被設為初始值著洼,如int型的為0,對象型的為null。
3. 父類的序列化特性
如果子類實現(xiàn)了Serializable接口而父類沒有實現(xiàn)身笤,那么父類不會被序列化豹悬,但是父類必須有默認的無參構造方法,否則會拋出InvalidClassException異常展鸡。如下圖所示

序列化異常

解決方案:想要將父類對象也序列化屿衅,就需要讓父類也實現(xiàn)Serializable接口;如果父類不實現(xiàn)的話莹弊,就需要有默認的無參構造函數(shù)涤久,并且父類的變量值都是默認聲明的值。

在父類沒有實現(xiàn)Serializable接口時忍弛,虛擬機不會序列化父對象响迂,而一個Java對象的初始化必須先初始化父對象,再初始化子對象细疚,反序列化也不例外蔗彤。所以在反序列化時,為了構造父對象疯兼,只能調用父類對象的無參構造函數(shù)作為默認的父對象然遏。因此當我們取父對象的變量值時,它的值是調用父類無參構造函數(shù)后的值吧彪。

使用Transient關鍵字可以使得字段不被序列化待侵,還有別的方法嗎?

根據(jù)父類對象序列化的規(guī)則姨裸,可以將不需要被序列化的字段抽取出來放到父類中秧倾,子類實現(xiàn)Serializable接口,父類不實現(xiàn)傀缩,根據(jù)父類序列化規(guī)則那先,父類的字段數(shù)據(jù)將不會被序列化。

抽象模型

4. 定制序列化方法

在序列化過程中赡艰,虛擬機會試圖調用對象類中的writeObjectreadObject方法售淡,進行用戶自定義的序列化和反序列化,如果沒有這樣的方法慷垮,則默認調用defaultWriteObject方法以及defaultReadObject方法勋又。用戶自定義的writeObjectreadObject方法運允許用戶控制序列化過程。比如可以在序列化過程中動態(tài)的改變序列化的數(shù)值换帜⌒ㄈ溃基于這個原理,可以在實際應用中得到使用惯驼,用于敏感字段的加密工作蹲嚣。

ObjectOutputStream使用getPrivateMethod

方法writeObject可以自定義用戶的序列化過程递瑰,如果聲明了private void writeObject(),它將會被ObjectOutputStream調用隙畜。盡管它們被外部類調用但是他們實際上是private的方法抖部。

writeObjectreadObject既不存在于java.lang.Object中,也沒有在Serializable中聲明议惰,那么ObjectOutputStream如何調用他們的慎颗?

ObjectOutputStream使用了反射尋找是否聲明了這兩個方法。并且ObjectOutputStream使用getPrivateMethod言询,所以這些方法必須聲明為private以至于可以被ObjectOutputStream調用俯萎。

為什么需要聲明為private類型

在兩個方法的開始處,你會發(fā)現(xiàn)調用了defaultWriteObject()defaultReadObject()运杭。它們的作用就是默認序列化進程夫啊,就像寫/讀所有的no-transientnon-static字段。通常來說辆憔,所有我們需要處理的字段都應該聲明為transient撇眯,這樣的話,defaultWriteObject/defaultReadObject便可以專注于其余字段虱咧,而我們則為特定的字段定制序列化熊榛。但是使用默認序列化方法并不是強制的。

需要注意的是:序列化和反序列化的writeXXX()readXXX()的順序需要對應腕巡。比如有多個字段都用writeInt()——序列化来候,那么readInt()需要按照順序將其賦值。

4.1. 使用transient和defaultWriteObject()定制序列化

public class EncryptUser implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userName;
    transient private String password;  //不進行序列化逸雹,需要自己手動處理的
    transient private String sex;
    
  //為節(jié)省篇幅 省略get/set/toString()方法

    private void writeObject(ObjectOutputStream oos) throws IOException {
        oos.defaultWriteObject();
        String password = this.password + ":加密";
        oos.writeUTF(password);  //將密碼手動處理加密后序列化
        System.out.println("EntryUser序列化成功:" + toString());
    }

    private void readObject(ObjectInputStream ios) throws IOException, ClassNotFoundException {
        ios.defaultReadObject();
        password = ios.readUTF() + "解密";
        System.out.println("EntryUser反序列化成功:" + toString());
    }
}

4.2. ObjectOutputStream.PutField定制序列化

public class EncryptUser implements Serializable {
    private static final long serialVersionUID = 1L;
    private String userName;
    private String password;  //不進行序列化,需要自己手動處理的

  //為節(jié)省篇幅 省略get/set/toString()方法

    private void writeObject(ObjectOutputStream oos) throws IOException {
        ObjectOutputStream.PutField putField = oos.putFields();//檢索寫入流的字段
        password = "加密:" + password;  //模擬加密
        //設置寫入流的字段
        putField.put("password", password);
        //將字段寫入流
        oos.writeFields();
    }

    private void readObject(ObjectInputStream ios) throws IOException, ClassNotFoundException {
        ObjectInputStream.GetField getField = ios.readFields();
        Object encryptPassword = getField.get("password", "");
        System.out.println("加密的字符串:" + encryptPassword);
        password = encryptPassword + "解密";
    }
}

4.3. 測試方法

 private static void writeObject() {
        try {
//            檢索用于緩沖要寫入流的持久性字段的對象云挟。 當調用writeFields方法時梆砸,字段將被寫入流。
            EncryptUser encryptUser = new EncryptUser();
            encryptUser.setUserName("tom");
            encryptUser.setPassword("tom245");
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./EncryptUser.txt"));
            objectOutputStream.writeObject(encryptUser);
            objectOutputStream.flush();
            ObjectInputStream objectInputStream=new ObjectInputStream(new FileInputStream("./EncryptUser.txt"));
            EncryptUser readObject = (EncryptUser)objectInputStream.readObject();
            System.out.println(readObject);
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

注:因為JDK1.7之后ObjectOutputStream實現(xiàn)了AutoCloseable接口园欣,會在try方法結束之后帖世,自動關閉資源。

5. 對象屬性序列化

如果一個類有引用類型的實例變量沸枯,那么這個引用也要實現(xiàn)Serializable接口日矫,否則會出現(xiàn):

引用屬性未實現(xiàn)序列化

可以使用transient關鍵字阻止該變量的序列化。

1.1.3 序列化的存儲

Java序列化機制為了節(jié)省磁盤空間绑榴,具有特定的存儲規(guī)則:當寫入文件為同一個對象時哪轿,并不會將對象的內容進行存儲,而是再次存儲一份引用翔怎。反序列化時窃诉,恢復引用關系杨耙。

序列化同一對象

public class RuleSerializable {
    public static void main(String[] args) {
        try {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.inf"));
            User user = new User();
            user.setName("tom");
            oos.writeObject(user);
            oos.flush();
            System.out.println("第一次讀取的長度:" + new File("user.inf").length());
            //第二次序列化后修改數(shù)據(jù)
            user.setName("lili");
            oos.writeObject(user);
            oos.flush();
            System.out.println("第二次讀取的長度:" + new File("user.inf").length());

            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("user.inf"));
            //反序列化
            User user1 = (User) objectInputStream.readObject();
            User user2 = (User) objectInputStream.readObject();
            System.out.println("兩個對象是否相等:" + (user1 == user2));
            System.out.println("反序列化的用戶名:"+user1.getName());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

我們看到最后的結果是輸出tom,原因是第一次寫入對象以后飘痛,第二次在試圖寫入的時候硕旗,虛擬機根據(jù)引用關系知道已經有一個對象內容寫入文件阴绢,因此只保存第二次寫的引用。所以在讀取時,獲取的是第一次保存的對象立砸。

序列化同一對象

2. Protostuff序列化

我們看到Java內置的序列化API Serializable,但是效率不是很高的积仗。Google提供了一個效率很高的序列化API Protobuf擅编,但是使用過于復雜。開源社區(qū)在Protobuf的基礎上封裝出Protostuff萌庆,在不丟失效率的前提上溶褪,使用更加簡單。一般情況下践险,protostuff序列化后的數(shù)據(jù)大小是Serializable的1/10之一猿妈,速度更是兩個量級以上。

2.1 protostuff序列化簡單使用

MAVEN依賴

    <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-runtime</artifactId>
            <version>1.6.0</version>
        </dependency>

        <dependency>
            <groupId>io.protostuff</groupId>
            <artifactId>protostuff-core</artifactId>
            <version>1.6.0</version>
        </dependency>

序列化:

public static <T> byte[] serializer(T obj) {
        Class<T> clazz = (Class<T>) obj.getClass();
        //本質上是一個數(shù)組對象
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            //獲取模板
            Schema<T> schema = RuntimeSchema.getSchema(clazz);
            //將Object對象裝換按照schema對象巍虫,轉化成byte[]對象
            byte[] bytes = ProtostuffIOUtil.toByteArray(obj, schema, buffer);
            return bytes;
        } catch (Exception e) {
            throw new RuntimeException("序列化失敗...");
        } finally {
            buffer.clear();
        }
    }
  1. 獲取傳入對象的class對象彭则;
  2. 獲取一個byte[]緩沖數(shù)組LinkBuffer
  3. 根據(jù)class對象獲取Schema對象
  4. Object對象序列化成byte[]數(shù)組占遥;

反序列化:

public static <T> T deserializer(byte[] data, Class<T> clazz) {
        if (data == null || data.length == 0) {
            throw new RuntimeException("反序列化失敗俯抖,byte[]不能為空");
        }
        T obj = null;
        try {
            obj = clazz.newInstance();
            Schema<T> schema = RuntimeSchema.getSchema(clazz);
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
        return obj;
    }
  1. 傳入byte[]數(shù)組和class對象;
  2. 通過反射初始化class對象瓦胎;
  3. 獲取Schema<T>對象芬萍;
  4. byte[]數(shù)組反序列化為Object對象;

注:RuntimeSchema.getSchema(clazz);實際上會將Schema<T>對象緩存搔啊。

2.2 protostuff定制開發(fā)

  1. 使用transient修飾就不用進行序列化柬祠;
  2. 定制序列化時,即用戶判斷什么情況下才進行序列化负芋,可以使用自定義Schema進行實現(xiàn)漫蛔。

1. 定義Java Bean類

此處使用了lombok插件,@Data標簽即實現(xiàn)get()set()方法旧蛾;@Builder標簽實現(xiàn)了建造者設計模式莽龟,即靜態(tài)內部類實現(xiàn)建造者角色,客戶端進行導演者角色锨天。

//地址類
@Builder(toBuilder = true)
@Data
public class Address {
    private String address;
    private String phone;
}
//用戶類
@Data
@Builder
public class Person {
    private String name;
    private Integer age;
    //表明該字段不進行序列化
    private transient String password;
    private List<Address> addressList;
}

2. 自定義Address的Schema

用戶定制化的開發(fā)毯盈,此處實現(xiàn)簡單,當address為null時病袄,不進行序列化奶镶。

//自定義序列化模板
public class AddressSchema implements Schema<Address> {
    @Override
    public String getFieldName(int number) {
        String ret = "";
        switch (number) {
            case 1:
                ret = "address";
                break;
            case 2:
                ret = "phone";
                break;
            default:
                break;
        }

        return ret;
    }

    @Override
    public int getFieldNumber(String name) {
        if ("address".equals(name)) {
            return 1;
        } else if ("phone".equals(name)) {
            return 2;
        }
        return 0;
    }

    //若是地址為null的話迟赃,不允許序列化
    @Override
    public boolean isInitialized(Address message) {
        if (message == null) {
            return false;
        }
        return false;
    }

    @Override
    public Address newMessage() {
        return Address.builder().build();
    }

    @Override
    public String messageName() {
        return Address.class.getSimpleName();
    }

    @Override
    public String messageFullName() {
        return Address.class.getName();
    }

    @Override
    public Class<? super Address> typeClass() {
        return Address.class;
    }

    //反序列化(輸入流中讀取數(shù)據(jù),寫入到message中)
    @Override
    public void mergeFrom(Input input, Address message) throws IOException {
        //在流中讀取數(shù)據(jù)(while循環(huán))
        while (true) {
            int number = input.readFieldNumber(this);//傳入的是模板文件
            switch (number) {
                case 0:
                    return;
                case 1:
                    message.setAddress(input.readString());  //設置address值
                    break;
                case 2:
                    message.setPhone(input.readString());  //設置phone值
                    break;
                default:
                    input.handleUnknownField(number, this);
            }
        }
    }

    //序列化(將對象設置到序列化的輸出流中)
    @Override
    public void writeTo(Output output, Address message) throws IOException {
        if (message.getAddress() == null) {
            throw new UninitializedMessageException(message, this);
        }
        //屬性序號厂镇、屬性內容纤壁,是否允許重復
        output.writeString(1, message.getAddress(), false);
        if (null != message.getPhone()) {
            output.writeString(2, message.getPhone(), false);
        }
    }
}

3. 編寫測試代碼
當序列化bjAddress時,因為address字段為null捺信,禁止其序列化酌媒。

public class ProtoTest {
    public static void main(String[] args) {
        Address shAddress = Address.builder().address("上海").phone("123123").build();
        Address bjAddress = Address.builder().phone("XXX").build();
        Person person = Person.builder().name("yxr").password("123").age(25).
                addressList(Arrays.asList(shAddress, bjAddress)).build();
        //序列化
        Schema<Person> schema = RuntimeSchema.createFrom(Person.class);
        //創(chuàng)建緩沖區(qū)
        LinkedBuffer buffer = LinkedBuffer.allocate(1024);
        //直接序列化數(shù)組
        byte[] bytes = ProtostuffIOUtil.toByteArray(person, schema, buffer);
        System.out.println("序列化:" + Arrays.toString(bytes));
        //反序列化
        Schema<Person> newSchema = RuntimeSchema.getSchema(Person.class);
        Person newPerson = newSchema.newMessage();  //創(chuàng)建了一個person對象
        ProtostuffIOUtil.mergeFrom(bytes, newPerson, newSchema);
        System.out.println("反序列化:" + newPerson);
        buffer.clear();  //釋放資源
        //創(chuàng)建自定義的Schema對象
        Schema<Address> addressSchema = new AddressSchema();
        byte[] bjArray = ProtostuffIOUtil.toByteArray(bjAddress, addressSchema, buffer);
        System.out.println("Address序列化:" + bjAddress);
        buffer.clear();
        byte[] shArray = ProtostuffIOUtil.toByteArray(shAddress, addressSchema, buffer);
        System.out.println(shAddress);
        Address newAddress = addressSchema.newMessage();
        ProtostuffIOUtil.mergeFrom(shArray, newAddress, addressSchema);
        System.out.println("Address反序列化:" + newAddress);
        buffer.clear();
    }
}

推薦參考:

什么是writeObject 和readObject?可定制的Serializable序列化過程

Protostuff定制Schema開發(fā)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末迄靠,一起剝皮案震驚了整個濱河市秒咨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌掌挚,老刑警劉巖雨席,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異吠式,居然都是意外死亡陡厘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門特占,熙熙樓的掌柜王于貴愁眉苦臉地迎上來糙置,“玉大人,你說我怎么就攤上這事是目“梗” “怎么了?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵懊纳,是天一觀的道長揉抵。 經常有香客問我,道長嗤疯,這世上最難降的妖魔是什么冤今? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮身弊,結果婚禮上,老公的妹妹穿的比我還像新娘列敲。我一直安慰自己阱佛,他們只是感情好,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布戴而。 她就那樣靜靜地躺著凑术,像睡著了一般。 火紅的嫁衣襯著肌膚如雪所意。 梳的紋絲不亂的頭發(fā)上淮逊,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天催首,我揣著相機與錄音,去河邊找鬼泄鹏。 笑死郎任,一個胖子當著我的面吹牛,可吹牛的內容都是我干的备籽。 我是一名探鬼主播舶治,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼车猬!你這毒婦竟也來了霉猛?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤珠闰,失蹤者是張志新(化名)和其女友劉穎惜浅,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體伏嗜,經...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡坛悉,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了阅仔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吹散。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖八酒,靈堂內的尸體忽然破棺而出空民,到底是詐尸還是另有隱情,我是刑警寧澤羞迷,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布界轩,位于F島的核電站,受9級特大地震影響衔瓮,放射性物質發(fā)生泄漏浊猾。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一热鞍、第九天 我趴在偏房一處隱蔽的房頂上張望葫慎。 院中可真熱鬧,春花似錦薇宠、人聲如沸偷办。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽椒涯。三九已至,卻和暖如春回梧,著一層夾襖步出監(jiān)牢的瞬間废岂,已是汗流浹背祖搓。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留湖苞,地道東北人拯欧。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像袒啼,于是被迫代替她去往敵國和親哈扮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

推薦閱讀更多精彩內容

  • JAVA序列化機制的深入研究 對象序列化的最主要的用處就是在傳遞,和保存對象(object)的時候,保證對象的完整...
    時待吾閱讀 10,837評論 0 24
  • 在Java中蚓再,我們可以通過多種方式來創(chuàng)建對象滑肉,并且只要對象沒有被回收我們都可以復用該對象。但是摘仅,我們創(chuàng)建出來的這些...
    懶癌正患者閱讀 1,523評論 0 12
  • “最好的教材就是源碼注釋靶庙,然后是大牛的總結⊥奘簦” 從今天開始寫博客六荒,目的很明確,梳理零碎的java知識矾端,總結并記錄下...
    蝸牛在北京閱讀 846評論 1 1
  • 一掏击、 序列化和反序列化概念 Serialization(序列化)是一種將對象以一連串的字節(jié)描述的過程;反序列化de...
    步積閱讀 1,437評論 0 10
  • 官方文檔理解 要使類的成員變量可以序列化和反序列化秩铆,必須實現(xiàn)Serializable接口砚亭。任何可序列化類的子類都是...
    獅_子歌歌閱讀 2,388評論 1 3