Java 虛擬機(jī) (四) - 類加載器

這是我們 java 虛擬機(jī)系列的第四篇文章, 類加載器

1.類加載器

jvm_1.png

Java 虛擬機(jī)的主要任務(wù)是裝載 class 文件并且執(zhí)行其中的字節(jié)碼邑彪。類加載器的作用是加載程序或 Java API 的 class 文件瞧毙,并將字節(jié)碼加載到執(zhí)行引擎。

在加載 class 文件時寄症, 為了防止加載進(jìn)來惡意的代碼宙彪,需要在類的加載器體系中去實(shí)現(xiàn)一些規(guī)則,保證在 Java 沙箱的安全模型有巧。

類加載其在 Java 沙箱中主要是三方面

  • 守護(hù)了被信任的類庫的邊界 - 通過雙親委托機(jī)制實(shí)現(xiàn)
  • 防止惡意代碼去干涉善意代碼 - 通過不同的命名空間去實(shí)現(xiàn)
  • 將代碼歸入某類(稱為保護(hù)域,該類確定了代碼可以進(jìn)行哪些操作释漆。

這三方面我們后續(xù)會一個一個說

首先我們先看看在 Java 虛擬機(jī)中的整個類加載器體系

2. 雙親委托機(jī)制

類加載器體系守護(hù)了被信任的類庫的邊界,這是通過分別使用不同的類加載器加載可靠包和不可靠包來實(shí)現(xiàn)的篮迎。

這些不同的類加載器之間的依賴關(guān)系男图,構(gòu)成了 Java 虛擬機(jī)中的雙親委托機(jī)制。所謂的雙親委托機(jī)制甜橱,是指類加載器請求另一個類加載器來加載類的過程逊笆。

jvm_2.png

上圖是類加載器雙親委托模型,我們可以看到岂傲,除了啟動類加載器以外的每一個類加載器难裆,都有一個 ”雙親“ 類加載器,在某個特定的類加載器試圖以常用的方式加載類以前,它會默認(rèn)將這個任務(wù) ”委托“ 給它的雙親 -- 請求它的雙親來加載這個類乃戈。這個雙親再依次請求它自己的雙親來加載這個類褂痰。這個委托的過程一直向上繼續(xù),直到達(dá)到啟動類加載器偏化。如果一個類加載器的雙親類加器有能力來加載這個類脐恩,則這個類加載器返回這個類。否則侦讨,這個類加載器試圖自己來加載這個類。

它們有著不同的啟動路徑

類加載器 路徑
Bootstrap ClassLoader 啟動類加載器 Load JRE\lib\rt.jar 或者 -Xbootclasspath 選項(xiàng)指定的 Jar 包
Extension ClassLoader 擴(kuò)展類加載器 Load JRE\lib\ext*.jar 或 -Djava.ext.dirs 指定目錄下的 Jar 包
Application ClassLoader 應(yīng)用程序類加載器 Load CLASSPATH 或 -Djava.class.path 所指定的目錄下的類和 Jar 包
User ClassLoader 自定義類加載器 通過 Java.lang.ClassLoader 的子類自定義加載 class

ClassLoader 的 loadClass 方法和 findClass 方法苟翻,如果是我們自定義 ClassLoader 的話韵卤,只需要重寫 findClass 方法即可

下面我們用一個例子來說明來加載器的雙親委托機(jī)制。我們自定義一個 ClassLoader 并復(fù)寫它的 findClass() 方法

  @Override
    protected Class findClass(String className) throws ClassNotFoundException {
        System.out.println("findClass className: " + className);
        byte[] classData;

        classData = getTypeFromBasePath(className);
        if (classData == null){
            throw new ClassNotFoundException();
        }

        // Parse it
        return defineClass(className, classData, 0, classData.length);
    }


    private byte[] getTypeFromBasePath(String typeName){
        FileInputStream fis;
        String fileName = path  + typeName.replace('.', File.separatorChar) + ".class";
        System.out.println("getTypeFromBasePath fileName :" + fileName);

        try {
            fis = new FileInputStream(fileName);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
            return  null;
        }

        BufferedInputStream bis = new BufferedInputStream(fis);
        ByteArrayOutputStream out = new ByteArrayOutputStream();

        try {
            int c = bis.read();
            while ( c != -1){
                out.write(c);
                c = bis.read();
            }

        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }

        return out.toByteArray();
    }
  
    

然后在我們通過 IDEA 編譯崇猫,將編譯出來的 Class 文件版本放到桌面沈条,并且指定路徑進(jìn)行加載。

jvm_3.png

然后在 main 方法中運(yùn)行诅炉,將路徑設(shè)置為 我們在上面的桌面 視圖加載 Test1 類

public static void main(String[] args) throws Exception {
        //  loadClass
        MyClassLoader loader1 = new MyClassLoader("loader1");

        loader1.setPath("/Users/yxhuang/Desktop/");

        Class<?> clazz = loader1.loadClass("com.yxhuang.jvm.bytecode.Test1");
        System.out.println("class name: " + clazz.getSimpleName() + " \nclass hashcode: " + clazz.hashCode() + " \nloader: " + clazz.getClassLoader().getClass().getSimpleName());
        Object object1 = clazz.newInstance();
        System.out.println(object1);

}

上面的例子蜡歹,我們將 MyClassLoader 命名為 loader1, 設(shè)置路徑為我們的電腦桌面。這時候運(yùn)行涕烧,看看輸出

class name: Test1 
class hashcode: 1265094477 
loader: AppClassLoader
com.yxhuang.jvm.bytecode.Test1@7ea987ac

上面輸出月而,我們看看 Test1 類文件已經(jīng)加載進(jìn)了 類加載器,但是打印出來议纯,我們看到 ClassLoader 是 AppClassLoader 而不是我們自定義的 MyClassLoader父款。 為什么會這樣呢,這就涉及到類的雙親委托機(jī)制了瞻凤。

當(dāng)我們用 loader1 視圖去加載 com.yxhuang.jvm.bytecode.Test1 這個類的時候憨攒,根據(jù)雙親委托機(jī)制,自定義的類加載器 MyClassLoader 會委托它的父加載器 AppClassLoader 去加載阀参, AppClassLoader 應(yīng)用類加載器又會委托它的父類加載器 Bootstrap ClassLoader 啟動類去加載肝集。而 Bootstrap ClassLoader 找不到這個類,然后讓 AppClassLoader 去加載蛛壳,還記得上面提到 AppClassLoader 加載的路徑是項(xiàng)目的 ClassPath, 這時候找到了 Test1 類并加載了它杏瞻,并沒有讓 MyClassLoader 去加載

jvm_4.png

現(xiàn)在,我們把 out/production/class 路徑里面的 Test1 刪掉炕吸,再次運(yùn)行查看結(jié)果
這時候的輸出是

findClass className: com.yxhuang.jvm.bytecode.Test1
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/bytecode/Test1.class
class name: Test1 
class hashcode: 1554874502 
loader: MyClassLoader
com.yxhuang.jvm.bytecode.Test1@6e0be858

看到上面的輸出伐憾,我們看到我們自定義的 MyClassLoader 被調(diào)用了,加載 Test1 的路徑是 /Users/yxhuang/Desktop/com/ , 類加載器也是我們自定義的 MyClassLoader

jvm_5.png

MyClassLoader 會委托給它的父類赫模,最后到 啟動類加載器树肃,然后 MyClassLoader 之上的類加載器沒有一個能加載到,最后只能是 MyClassLoader 來加載瀑罗。

雙親委托機(jī)制可以保證父類先加載 Class 文件胸嘴,特別是 jdk 里面的類雏掠,保證 jdk 類的類優(yōu)先被啟動類加載器加載,防止惡意代碼偽裝成 jdk 的類去破壞 jvm 的運(yùn)行劣像。

雙親委托機(jī)制還有下面的一些特點(diǎn):

  • 1.如果沒有顯示地傳遞一個雙親類裝載器給用戶自定義的類裝載器的構(gòu)造方法乡话,系統(tǒng)裝載器就默認(rèn)被指定為雙親。
  • 2. 如果傳遞到構(gòu)造方法的是一個已有的用戶自定義類型裝載器的引用耳奕,該用戶自定義裝載器就被作為雙親绑青。
  • 3.如果傳遞的方法是一個 null, 啟動類裝載器就是雙親。
  • 4.在類裝載器之間具有了委派關(guān)系屋群,首先發(fā)起裝載要求的類裝載器不必是定義該類的類裝載器闸婴。

當(dāng)時雙親委托機(jī)制,也有它不足的地方芍躏,在不需要雙親委托機(jī)制的地方邪乍,需要上下文類加載器。關(guān)于上下文類加載器对竣,后面我們會講到庇楞,這里先跳過。

下面我們先看看命名空間

3. 類加器的命名空間

下面我們通過實(shí)例代碼否纬,說明命名空間

先定義一個 Person 類

public class Person {
    private Person person;

    public Person() {
    }

    public void setPerson(Object object){
        System.out.println("setPerson " + object.getClass().getSimpleName());
        this.person = (Person) object;
    }
}

用 IDEA 編譯成 class 文件吕晌,將編譯出來的 Class 文件版本放到桌面谢床,并且指定路徑進(jìn)行加載许布。然后將 Persion 的 class 文件 刪除,同時注釋 Person汰规。

下面是命名空間的測試類, 設(shè)置加載路徑谬俄,用兩個不同的類加載器去加載 com.yxhuang.jvm.classloader.Person 類柏靶,然后通過反射,調(diào)用 class1 的 setPerson 方法溃论。

public class NameSpaceLoaderTest {

    public static void main(String[] arg) throws Exception {
        MyClassLoader classLoader1 = new MyClassLoader("classloader1");
        MyClassLoader classLoader2 = new MyClassLoader("classloader2");

        classLoader1.setPath("/Users/yxhuang/Desktop/");
        classLoader2.setPath("/Users/yxhuang/Desktop/");

        Class<?> class1 = classLoader1.loadClass("com.yxhuang.jvm.classloader.Person");
        Class<?> class2 = classLoader2.loadClass("com.yxhuang.jvm.classloader.Person");

        System.out.println("class1 : " + class1.getSimpleName() + " " +  class1.getClassLoader().toString());

        System.out.println("class2 : " + class2.getSimpleName() + " " +  class2.getClassLoader().toString());

        System.out.println(class1 == class2);

        Object object1 = class1.newInstance();
        Object object2 = class2.newInstance();

        Method method = class1.getMethod("setPerson", Object.class);
        method.invoke(object1, object2);

    }
}

然后屎蜓,我們看看輸出

findClass className: com.yxhuang.jvm.classloader.Person
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/classloader/Person.class

findClass className: com.yxhuang.jvm.classloader.Person
getTypeFromBasePath fileName :/Users/yxhuang/Desktop/com/yxhuang/jvm/classloader/Person.class

class1 : Person com.yxhuang.jvm.classloader.MyClassLoader@42a57993

class2 : Person com.yxhuang.jvm.classloader.MyClassLoader@6bc7c054

false

// 還會拋出異常
Caused by: java.lang.ClassCastException: com.yxhuang.jvm.classloader.Person cannot be cast to com.yxhuang.jvm.classloader.Person

根據(jù)上面的打印,我們可以知道钥勋, class1 和 class2 都是 Person 類炬转,但是 class1 == class2 是 false 的,說明他們不是同一個類算灸。

將 class1 和 class2 通過 newInstance() 方法生成對應(yīng)的 object1 和 object2 對象扼劈,這也都是 Person 類的對象。
在調(diào)用反射將 object1 的 setPerson 方法會拋出異常

public void setPerson(Object object){
        System.out.println("setPerson " + object.getClass().getSimpleName());
        this.person = (Person) object;
}

拋出的異常是說 Person 對象不能強(qiáng)轉(zhuǎn)成 Person 對象菲驴。這個異常就很奇怪了荐吵,那為什么會出現(xiàn)這個異常,那就要說到 java 虛擬機(jī)里面的命名空間了。
因?yàn)檫@兩個對象加載的虛擬機(jī)不一樣先煎,導(dǎo)致命名空間不一樣導(dǎo)致的贼涩。

命名空間是表示當(dāng)前類的加載器的命名空間,是由當(dāng)前類轉(zhuǎn)加載器是自己的初始類加載器的類型名稱組成的薯蝎。

命名空間的作用是通過不同的命名空間遥倦,防止惡意代碼去干涉其他代碼。在 Java 虛擬機(jī)中占锯,在同一個命名空間內(nèi)的類可以之間進(jìn)行交互袒哥,而不同的命名空間中的類察覺不到彼此的存在。

每個類裝載器都有自己的命名空間烟央,其中維護(hù)者由它裝載的類型统诺。所以一個 Java 程序可以多次裝載具有一個全限定名的多個類型。這樣一個類的全限定名就不足以確定在一個 Java 虛擬機(jī)中的唯一性疑俭。因此,當(dāng)多個類裝載器都裝載了同名的類型時婿失,為了唯一地標(biāo)識該類型钞艇,還要在類型名稱前加上裝載器該類(指出了它所位于的命名空間)的類裝載器的標(biāo)識。

上面 Person 的這個例子就說明豪硅,一個類的全限定名 com.yxhuang.jvm.classloader.Person 不能確定它的唯一性哩照,我們可以用另外一個類加載器去再次加載這個類。

綜上所述懒浮,如果想要確定一個類是否是唯一的或者說判斷兩個類是否相等飘弧,就需要他們的類加載器為同一個累加器,并且命名空間是一致的砚著。

關(guān)于命名空間的一些論述

    1. 每個類裝載器都有自己的命名空間次伶,命名空間由該裝載器及其父裝載器所裝載的類組成;
    1. 在同一個命名空間中稽穆,不會出現(xiàn)類的完整姓名(包括類的包名)相同的兩個類冠王;
    1. 在不同的命名空間中,有可能會出現(xiàn)類的完整名字(包含類的包名)相同的兩個類舌镶。

類裝載器和這個類本身一起共同確立在 Java 虛擬機(jī)中的唯一性柱彻,每一個類裝載器,都有一個獨(dú)立的命名空間餐胀。
也就是說哟楷,比較兩個類是否”相等“,只有這兩個類是由同一個類裝載器的前提下否灾,否則卖擅,即使這兩個類來源于同一個 Class 文件,被同一個 Java 虛擬機(jī)加載,只要加載它們的類裝載器不同磨镶,那這兩個類就必定不相等溃蔫。

不同的加載器實(shí)例加載的類被認(rèn)為是不同的類

在 JVM 的實(shí)現(xiàn)中有一條隱含的規(guī)則,默認(rèn)情況下琳猫,如果一個類由類加載器 A 加載伟叛,那么這個類的依賴類也是由相同的類加載器加載

上面的幾條論述在例子中也有體現(xiàn)。

4 自定義類加載器

4.1 自定義類加載器

如果想要自定義類加載器脐嫂,只需要繼承 ClassLoader 并且重寫它的 findClass() 方法统刮。

在 findClass() 方法里面根據(jù)路徑去加載相應(yīng)的 Class 文件流,然后將數(shù)據(jù)傳遞給 ClassLoader 自帶的 defineClass() 方法账千,defineClass() 會將Class 流文件轉(zhuǎn)成 Class 類的實(shí)例侥蒙。

@Override
protected Class findClass(String className) throws ClassNotFoundException {
    System.out.println("findClass className: " + className);
    byte[] classData;
    
    // 指定路徑加載 Class 流文件
    classData = getTypeFromBasePath(className);
    if (classData == null){
        throw new ClassNotFoundException();
    }

    // Parse it 將流文件轉(zhuǎn)成一個 Class 類實(shí)例
    return defineClass(className, classData, 0, classData.length);
}

除此之外,必須要了解 ClassLoader 里面的 loadClass() 方法

4.2 loadClass() 方法

在我們自定義了 ClassLoader 之后匀奏,會調(diào)用 loadClass() 方法去加載想要加載的類鞭衩。

  • loadClass() 的基本工作方式:
    給定需要查找的類型的全限定名, loadClass()方法會用某種方式找到或生成字節(jié)數(shù)組到娃善,里面的數(shù)據(jù)采用 Java Class 文件格式(用該格式定義類型)论衍。如果 loadClass() 無法找到或生成這些字節(jié),就會拋出 ClassNotFoundException 異常聚磺。否則坯台,loadClass() 會傳遞這個自己數(shù)組到 ClassLoader 聲明的某一個 defineClass() 方法。通過把這些字節(jié)數(shù)組傳遞給
    defineClass(),loadClass() 會要求虛擬機(jī)把傳入的字節(jié)數(shù)組導(dǎo)入這個用戶自定義的類裝載器的命名中間中去瘫寝。

  • loadClass 的步驟:

    • 1.查看是否請求的類型已經(jīng)被這個類裝載器裝載進(jìn)命名空間(提供 findLoadedClass())方法的工作方式
    • 2.否則蜒蕾,委派到這個類裝載器的雙親裝載器。如果雙親返回了一個 Class 實(shí)例焕阿,就把這個 Class 實(shí)例返回咪啡。
      1. 否則,調(diào)用 findClass(), findClass() 會試圖尋找或者生成一個字節(jié)數(shù)組捣鲸,內(nèi)容采用 Java Class 文件格式(它定義了所需要的類型)瑟匆。如果成功,findClass() 把這個字節(jié)傳遞給 defineClass() 栽惶,后者試圖導(dǎo)入這個類型愁溜,返回一個 Class 實(shí)例。 如果 findClass() 返回一個 Class 實(shí)例外厂,loadClass() 就會把這個實(shí)例返回冕象。
      1. 否則, findClass() 拋出某些異常來中止處理汁蝶,而且 loadClass() 也會拋出異常中止渐扮。
 
public abstract class ClassLoader {

    //每個類加載器都有個父加載器
    private final ClassLoader parent;
    
    public Class<?> loadClass(String name) {
  
        //查找一下這個類是不是已經(jīng)加載過了
        Class<?> c = findLoadedClass(name);
        
        //如果沒有加載過
        if( c == null ){
          //先委托給父加載器去加載论悴,注意這是個遞歸調(diào)用
          if (parent != null) {
              c = parent.loadClass(name);
          }else {
              // 如果父加載器為空,查找Bootstrap加載器是不是加載過了
              c = findBootstrapClassOrNull(name);
          }
        }
        // 如果父加載器沒加載成功墓律,調(diào)用自己的findClass去加載
        if (c == null) {
            c = findClass(name);
        }
        
        return c膀估;
    }
    
    protected Class<?> findClass(String name){
       //1. 根據(jù)傳入的類名name,到在特定目錄下去尋找類文件耻讽,把.class文件讀入內(nèi)存
          ...
          
       //2. 調(diào)用defineClass將字節(jié)數(shù)組轉(zhuǎn)成Class對象
       return defineClass(buf, off, len)察纯;
    }
    
    // 將字節(jié)碼數(shù)組解析成一個Class對象,用native方法實(shí)現(xiàn)
    protected final Class<?> defineClass(byte[] b, int off, int len){
       ...
    }
}

5 線程上下文類加載器

雙親委托機(jī)制不適用的場景下针肥,需要使用到 上下文類加載器(Thread Context ClassLoader)

場景是有基礎(chǔ)類要調(diào)用用戶代碼(Service Provider Interface, SPI)

線程上下文加載器通過 Thread 類的 setContextClassLoader() 方法進(jìn)行設(shè)置饼记,如果創(chuàng)建線程還未設(shè)置,就會從父線程中繼承一個慰枕,如果在應(yīng)用程序的全局范圍都沒有設(shè)置過的話具则,那這個類裝載器默認(rèn)是應(yīng)用類加載器。

6.獲取 ClassLoader 的途徑

獲取當(dāng)前類的 ClassLoader: clazz.getClassLoader()

獲取當(dāng)前線程上下文的 ClassLoader: Thread.currentThread().getContextClassLoader()

獲取系統(tǒng)的 ClassLoader : ClassLoader.getSystemClassLoader()

獲取調(diào)用者的 ClassLoader: DriverManager.getCallerClassLoader()

7.參考

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末具帮,一起剝皮案震驚了整個濱河市博肋,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蜂厅,老刑警劉巖束昵,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異葛峻,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)巴比,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門术奖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人轻绞,你說我怎么就攤上這事采记。” “怎么了政勃?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵唧龄,是天一觀的道長。 經(jīng)常有香客問我奸远,道長既棺,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任懒叛,我火速辦了婚禮丸冕,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘薛窥。我一直安慰自己胖烛,他們只是感情好眼姐,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著佩番,像睡著了一般众旗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上趟畏,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天贡歧,我揣著相機(jī)與錄音,去河邊找鬼拱镐。 笑死艘款,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的沃琅。 我是一名探鬼主播哗咆,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼益眉!你這毒婦竟也來了晌柬?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤郭脂,失蹤者是張志新(化名)和其女友劉穎年碘,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體展鸡,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡屿衅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了莹弊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涤久。...
    茶點(diǎn)故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖忍弛,靈堂內(nèi)的尸體忽然破棺而出响迂,到底是詐尸還是另有隱情,我是刑警寧澤细疚,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布蔗彤,位于F島的核電站,受9級特大地震影響疯兼,放射性物質(zhì)發(fā)生泄漏然遏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一镇防、第九天 我趴在偏房一處隱蔽的房頂上張望啦鸣。 院中可真熱鬧,春花似錦来氧、人聲如沸诫给。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽中狂。三九已至凫碌,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間胃榕,已是汗流浹背盛险。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留勋又,地道東北人苦掘。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像楔壤,于是被迫代替她去往敵國和親鹤啡。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,577評論 2 353