JVM自定義類加載器加載指定classPath下的所有class及jar

一葵腹、JVM中的類加載器類型

從Java虛擬機(jī)的角度講责鳍,只有兩種不同的類加載器:?jiǎn)?dòng)類加載器和其他類加載器。
  1.啟動(dòng)類加載器(Boostrap ClassLoader):這個(gè)是由c++實(shí)現(xiàn)的雀瓢,主要負(fù)責(zé)JAVA_HOME/lib目錄下的核心 api 或 -Xbootclasspath 選項(xiàng)指定的jar包裝入工作陡舅。
  2.其他類加載器:由java實(shí)現(xiàn),可以在方法區(qū)找到其Class對(duì)象叼旋。這里又細(xì)分為幾個(gè)加載器
    a).擴(kuò)展類加載器(Extension ClassLoader):負(fù)責(zé)用于加載JAVA_HOME/lib/ext目錄中的仇哆,或者被-Djava.ext.dirs系統(tǒng)變量指定所指定的路徑中所有類庫(kù)(jar),開發(fā)者可以直接使用擴(kuò)展類加載器夫植。java.ext.dirs系統(tǒng)變量所指定的路徑的可以通過System.getProperty("java.ext.dirs")來查看讹剔。
    b).應(yīng)用程序類加載器(Application ClassLoader):負(fù)責(zé)java -classpath或-Djava.class.path所指的目錄下的類與jar包裝入工作。開發(fā)者可以直接使用這個(gè)類加載器详民。在沒有指定自定義類加載器的情況下辟拷,這就是程序的默認(rèn)加載器。
    c).自定義類加載器(User ClassLoader):在程序運(yùn)行期間, 通過java.lang.ClassLoader的子類動(dòng)態(tài)加載class文件, 體現(xiàn)java動(dòng)態(tài)實(shí)時(shí)類裝入特性阐斜。

這四個(gè)類加載器的層級(jí)關(guān)系衫冻,如下圖所示。

image

二谒出、為什么要自定義類加載器

  1. 區(qū)分同名的類:假定在tomcat 應(yīng)用服務(wù)器隅俘,上面部署著許多獨(dú)立的應(yīng)用,同時(shí)他們擁有許多同名卻不同版本的類笤喳。要區(qū)分不同版本的類當(dāng)然是需要每個(gè)應(yīng)用都擁有自己獨(dú)立的類加載器了为居,否則無法區(qū)分使用的具體是哪一個(gè)。
  2. 類庫(kù)共享:每個(gè)web應(yīng)用在tomcat中都可以使用自己版本的jar杀狡。但存在如Servlet-api.jar蒙畴,java原生的包和自定義添加的Java類庫(kù)可以相互共享。
  3. 加強(qiáng)類:類加載器可以在 loadClass 時(shí)對(duì) class 進(jìn)行重寫和覆蓋呜象,在此期間就可以對(duì)類進(jìn)行功能性的增強(qiáng)膳凝。比如使用javassist對(duì)class進(jìn)行功能添加和修改,或者添加面向切面編程時(shí)用到的動(dòng)態(tài)代理恭陡,以及 debug 等原理蹬音。
  4. 熱替換:在應(yīng)用正在運(yùn)行的時(shí)候升級(jí)軟件,不需要重新啟動(dòng)應(yīng)用休玩。比如toccat服務(wù)器中JSP更新替換著淆。

三劫狠、自定義類加載器

3.1 ClassLoader實(shí)現(xiàn)自定義類加載器相關(guān)方法說明

要實(shí)現(xiàn)自定義類加載器需要先繼承ClassLoader,ClassLoader類是一個(gè)抽象類永部,負(fù)責(zé)加載classes的對(duì)象独泞。自定義ClassLoader中至少需要了解其中的三個(gè)的方法: loadClass,findClass,defineClass。

public Class<?> loadClass(String name) throws ClassNotFoundException {
return loadClass(name, false);
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
protected final Class<?> defineClass(String name, byte[] b, int off, int len)
throws ClassFormatError
{
return defineClass(name, b, off, len, null);
}

...
   loadClass:JVM在加載類的時(shí)候苔埋,都是通過ClassLoader的loadClass()方法來加載class的懦砂,loadClass使用雙親委派模式。如果要改變雙親委派模式讲坎,可以修改loadClass來改變class的加載方式孕惜。雙親委派模式這里就不贅述了。
  findClass:ClassLoader通過findClass()方法來加載類晨炕。自定義類加載器實(shí)現(xiàn)這個(gè)方法來加載需要的類衫画,比如指定路徑下的文件,字節(jié)流等瓮栗。
  definedClass:definedClass在findClass中使用削罩,通過調(diào)用傳進(jìn)去一個(gè)Class文件的字節(jié)數(shù)組,就可以方法區(qū)生成一個(gè)Class對(duì)象费奸,也就是findClass實(shí)現(xiàn)了類加載的功能了弥激。

貼上一段ClassLoader中l(wèi)oadClass源碼,見見真面目...

protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

源碼說明...

/**
* Loads the class with the specified <a href="#name">binary name</a>. The
* default implementation of this method searches for classes in the
* following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class
* has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) <tt>loadClass</tt>} method
* on the parent class loader. If the parent is <tt>null</tt> the class
* loader built-in to the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the
* class. </p></li>
*
* </ol>
*
* <p> If the class was found using the above steps, and the
* <tt>resolve</tt> flag is true, this method will then invoke the {@link
* #resolveClass(Class)} method on the resulting <tt>Class</tt> object.
*
* <p> Subclasses of <tt>ClassLoader</tt> are encouraged to override {@link
* #findClass(String)}, rather than this method. </p>
*
* <p> Unless overridden, this method synchronizes on the result of
* {@link #getClassLoadingLock <tt>getClassLoadingLock</tt>} method
* during the entire class loading process.
*
* @param name
* The <a href="#name">binary name</a> of the class
*
* @param resolve
* If <tt>true</tt> then resolve the class
*
* @return The resulting <tt>Class</tt> object
*
* @throws ClassNotFoundException
* If the class could not be found
*/

翻譯過來大概是:使用指定的二進(jìn)制名稱來加載類愿阐,這個(gè)方法的默認(rèn)實(shí)現(xiàn)按照以下順序查找類: 調(diào)用findLoadedClass(String)方法檢查這個(gè)類是否被加載過 使用父加載器調(diào)用loadClass(String)方法微服,如果父加載器為Null,類加載器裝載虛擬機(jī)內(nèi)置的加載器調(diào)用findClass(String)方法裝載類缨历, 如果以蕴,按照以上的步驟成功的找到對(duì)應(yīng)的類,并且該方法接收的resolve參數(shù)的值為true,那么就調(diào)用resolveClass(Class)方法來處理類辛孵。 ClassLoader的子類最好覆蓋findClass(String)而不是這個(gè)方法丛肮。 除非被重寫,這個(gè)方法默認(rèn)在整個(gè)裝載過程中都是同步的(線程安全的)魄缚。

resolveClass:Class載入必須鏈接(link)宝与,鏈接指的是把單一的Class加入到有繼承關(guān)系的類樹中。這個(gè)方法給Classloader用來鏈接一個(gè)類冶匹,如果這個(gè)類已經(jīng)被鏈接過了习劫,那么這個(gè)方法只做一個(gè)簡(jiǎn)單的返回。否則徙硅,這個(gè)類將被按照 Java規(guī)范中的Execution描述進(jìn)行鏈接榜聂。

3.2 自定義類加載器實(shí)現(xiàn)

按照3.1的說明,繼承ClassLoader后重寫了findClass方法加載指定路徑上的class嗓蘑。先貼上自定義類加載器须肆。

package com.chenerzhu.learning.classloader;

import java.nio.file.Files;
import java.nio.file.Paths;

/**
 * @author chenerzhu
 * @create 2018-10-04 10:47
 **/
public class MyClassLoader extends ClassLoader {
    private String path;

    public MyClassLoader(String path) {
        this.path = path;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String name) {
        try {
            return Files.readAllBytes(Paths.get(path));
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

以上就是自定義的類加載器了,實(shí)現(xiàn)的功能是加載指定路徑的class桩皿。再看看如何使用豌汇。

package com.chenerzhu.learning.classloader;

import org.junit.Test;

/**
 * Created by chenerzhu on 2018/10/4.
 */
public class MyClassLoaderTest {
    @Test
    public void testClassLoader() throws Exception {
        MyClassLoader myClassLoader = new MyClassLoader("src/test/resources/bean/Hello.class");
        Class clazz = myClassLoader.loadClass("com.chenerzhu.learning.classloader.bean.Hello");
        Object obj = clazz.newInstance();
        System.out.println(obj);
        System.out.println(obj.getClass().getClassLoader());
    }
}

首先通過構(gòu)造方法創(chuàng)建MyClassLoader對(duì)象myClassLoader,指定加載src/test/resources/bean/Hello.class路徑的Hello.class(當(dāng)然這里只是個(gè)例子泄隔,直接指定一個(gè)class的路徑了)拒贱。然后通過myClassLoader方法loadClass加載Hello的Class對(duì)象,最后實(shí)例化對(duì)象佛嬉。以下是輸出結(jié)果逻澳,看得出來實(shí)例化成功了,并且類加載器使用的是MyClassLoader暖呕。

   com.chenerzhu.learning.classloader.bean.Hello@2b2948e2
   com.chenerzhu.learning.classloader.MyClassLoader@335eadca

四斜做、類Class卸載

JVM中class和Meta信息存放在PermGen space區(qū)域(JDK1.8之后存放在MateSpace中)。如果加載的class文件很多湾揽,那么可能導(dǎo)致元數(shù)據(jù)空間溢出瓤逼。引起java.lang.OutOfMemory異常。對(duì)于有些Class我們可能只需要使用一次库物,就不再需要了霸旗,也可能我們修改了class文件,我們需要重新加載 newclass戚揭,那么oldclass就不再需要了诱告。所以需要在JVM中卸載(unload)類Class。
  JVM中的Class只有滿足以下三個(gè)條件民晒,才能被GC回收精居,也就是該Class被卸載(unload):
1. 該類所有的實(shí)例都已經(jīng)被GC。
2. 該類的java.lang.Class對(duì)象沒有在任何地方被引用镀虐。
3. 加載該類的ClassLoader實(shí)例已經(jīng)被GC箱蟆。

很容易理解,就是要被卸載的類的ClassLoader實(shí)例已經(jīng)被GC并且本身不存在任何相關(guān)的引用就可以被卸載了刮便,也就是JVM清除了類在方法區(qū)內(nèi)的二進(jìn)制數(shù)據(jù)空猜。
  JVM自帶的類加載器所加載的類,在虛擬機(jī)的生命周期中恨旱,會(huì)始終引用這些類加載器辈毯,而這些類加載器則會(huì)始終引用它們所加載的類的Class對(duì)象。因此這些Class對(duì)象始終是可觸及的搜贤,不會(huì)被卸載谆沃。而用戶自定義的類加載器加載的類是可以被卸載的。雖然滿足以上三個(gè)條件Class可以被卸載仪芒,但是GC的時(shí)機(jī)我們是不可控的唁影,那么同樣的我們對(duì)于Class的卸載也是不可控的耕陷。

五、JVM自定義類加載器加載指定classPath下的所有class及jar

經(jīng)過以上幾個(gè)點(diǎn)的說明据沈,現(xiàn)在可以實(shí)現(xiàn)JVM自定義類加載器加載指定classPath下的所有class及jar了哟沫。這里沒有限制class和jar的位置,只要是classPath路徑下的都會(huì)被加載進(jìn)JVM锌介,而一些web應(yīng)用服務(wù)器加載是有限定的嗜诀,比如tomcat加載的是每個(gè)應(yīng)用classPath+“/classes”加載class,classPath+“/lib”加載jar孔祸。以下就是代碼啦...

package com.chenerzhu.learning.classloader;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Enumeration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

/**
 * @author chenerzhu
 * @create 2018-10-04 12:24
 **/
public class ClassPathClassLoader extends ClassLoader{

    private static Map<String, byte[]> classMap = new ConcurrentHashMap<>();
    private String classPath;

    public ClassPathClassLoader() {
    }

    public ClassPathClassLoader(String classPath) {
        if (classPath.endsWith(File.separator)) {
            this.classPath = classPath;
        } else {
            this.classPath = classPath + File.separator;
        }
        preReadClassFile();
        preReadJarFile();
    }

    public static boolean addClass(String className, byte[] byteCode) {
        if (!classMap.containsKey(className)) {
            classMap.put(className, byteCode);
            return true;
        }
        return false;
    }

    /**
     * 這里僅僅卸載了myclassLoader的classMap中的class,虛擬機(jī)中的
     * Class的卸載是不可控的
     * 自定義類的卸載需要MyClassLoader不存在引用等條件
     * @param className
     * @return
     */
    public static boolean unloadClass(String className) {
        if (classMap.containsKey(className)) {
            classMap.remove(className);
            return true;
        }
        return false;
    }

    /**
     * 遵守雙親委托規(guī)則
     */
    @Override
    protected Class<?> findClass(String name) {
        try {
            byte[] result = getClass(name);
            if (result == null) {
                throw new ClassNotFoundException();
            } else {
                return defineClass(name, result, 0, result.length);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    private byte[] getClass(String className) {
        if (classMap.containsKey(className)) {
            return classMap.get(className);
        } else {
            return null;
        }
    }

    private void preReadClassFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanClassFile(file);
            }
        }
    }

    private void scanClassFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".class")) {
                try {
                    byte[] byteCode = Files.readAllBytes(Paths.get(file.getAbsolutePath()));
                    String className = file.getAbsolutePath().replace(classPath, "")
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    addClass(className, byteCode);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanClassFile(f);
                }
            }
        }
    }

    private void preReadJarFile() {
        File[] files = new File(classPath).listFiles();
        if (files != null) {
            for (File file : files) {
                scanJarFile(file);
            }
        }
    }

    private void readJAR(JarFile jar) throws IOException {
        Enumeration<JarEntry> en = jar.entries();
        while (en.hasMoreElements()) {
            JarEntry je = en.nextElement();
            je.getName();
            String name = je.getName();
            if (name.endsWith(".class")) {
                //String className = name.replace(File.separator, ".").replace(".class", "");
                String className = name.replace("\\", ".")
                        .replace("/", ".")
                        .replace(".class", "");
                InputStream input = null;
                ByteArrayOutputStream baos = null;
                try {
                    input = jar.getInputStream(je);
                    baos = new ByteArrayOutputStream();
                    int bufferSize = 1024;
                    byte[] buffer = new byte[bufferSize];
                    int bytesNumRead = 0;
                    while ((bytesNumRead = input.read(buffer)) != -1) {
                        baos.write(buffer, 0, bytesNumRead);
                    }
                    addClass(className, baos.toByteArray());
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    if (baos != null) {
                        baos.close();
                    }
                    if (input != null) {
                        input.close();
                    }
                }
            }
        }
    }

    private void scanJarFile(File file) {
        if (file.exists()) {
            if (file.isFile() && file.getName().endsWith(".jar")) {
                try {
                    readJAR(new JarFile(file));
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } else if (file.isDirectory()) {
                for (File f : file.listFiles()) {
                    scanJarFile(f);
                }
            }
        }
    }


    public void addJar(String jarPath) throws IOException {
        File file = new File(jarPath);
        if (file.exists()) {
            JarFile jar = new JarFile(file);
            readJAR(jar);
        }
    }
}

如何使用的代碼就不貼了隆敢,和3.2節(jié)自定義類加載器的使用方式一樣。只是構(gòu)造方法的參數(shù)變成classPath了崔慧,篇末有代碼拂蝎。當(dāng)創(chuàng)建MyClassLoader對(duì)象時(shí),會(huì)自動(dòng)添加指定classPath下面的所有class和jar里面的class到classMap中尊浪,classMap維護(hù)className和classCode字節(jié)碼的關(guān)系匣屡,只是個(gè)緩沖作用,避免每次都從文件中讀取拇涤。自定義類加載器每次loadClass都會(huì)首先在JVM中找是否已經(jīng)加載className的類捣作,如果不存在就會(huì)到classMap中取,如果取不到就是加載錯(cuò)誤了鹅士。

六券躁、最后

至此,JVM自定義類加載器加載指定classPath下的所有class及jar已經(jīng)完成了掉盅。這篇博文花了兩天才寫完也拜,在寫的過程中有意識(shí)地去了解了許多代碼的細(xì)節(jié),收獲也很多趾痘。本來最近僅僅是想實(shí)現(xiàn)Quartz控制臺(tái)頁面任務(wù)添加支持動(dòng)態(tài)class慢哈,結(jié)果不知不覺跑到類加載器的坑了,在此也趁這個(gè)機(jī)會(huì)總結(jié)一遍永票。當(dāng)然以上內(nèi)容并不能保證正確卵贱,所以希望大家看到錯(cuò)誤能夠指出,幫助我更正已有的認(rèn)知侣集,共同進(jìn)步键俱。。世分。

本文的代碼已經(jīng)上傳github:https://github.com/chenerzhu/learning/tree/master/classloader 歡迎下載和指正编振。

參考文章:

深度分析Java的ClassLoader機(jī)制(源碼級(jí)別)

自定義類加載器-從.class和.jar中讀取

Class熱替換與卸載

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市臭埋,隨后出現(xiàn)的幾起案子踪央,更是在濱河造成了極大的恐慌臀玄,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,599評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杯瞻,死亡現(xiàn)場(chǎng)離奇詭異镐牺,居然都是意外死亡炫掐,警方通過查閱死者的電腦和手機(jī)魁莉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,629評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來募胃,“玉大人旗唁,你說我怎么就攤上這事”允” “怎么了检疫?”我有些...
    開封第一講書人閱讀 158,084評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)祷嘶。 經(jīng)常有香客問我屎媳,道長(zhǎng),這世上最難降的妖魔是什么论巍? 我笑而不...
    開封第一講書人閱讀 56,708評(píng)論 1 284
  • 正文 為了忘掉前任烛谊,我火速辦了婚禮,結(jié)果婚禮上嘉汰,老公的妹妹穿的比我還像新娘丹禀。我一直安慰自己,他們只是感情好鞋怀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,813評(píng)論 6 386
  • 文/花漫 我一把揭開白布双泪。 她就那樣靜靜地躺著,像睡著了一般密似。 火紅的嫁衣襯著肌膚如雪焙矛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 50,021評(píng)論 1 291
  • 那天残腌,我揣著相機(jī)與錄音村斟,去河邊找鬼。 笑死废累,一個(gè)胖子當(dāng)著我的面吹牛邓梅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播邑滨,決...
    沈念sama閱讀 39,120評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼日缨,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了掖看?” 一聲冷哼從身側(cè)響起匣距,我...
    開封第一講書人閱讀 37,866評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤面哥,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后毅待,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體尚卫,經(jīng)...
    沈念sama閱讀 44,308評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,633評(píng)論 2 327
  • 正文 我和宋清朗相戀三年尸红,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了吱涉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,768評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡外里,死狀恐怖怎爵,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情盅蝗,我是刑警寧澤鳖链,帶...
    沈念sama閱讀 34,461評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站墩莫,受9級(jí)特大地震影響芙委,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狂秦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,094評(píng)論 3 317
  • 文/蒙蒙 一灌侣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧故痊,春花似錦顶瞳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,850評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至戴甩,卻和暖如春符喝,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背甜孤。 一陣腳步聲響...
    開封第一講書人閱讀 32,082評(píng)論 1 267
  • 我被黑心中介騙來泰國(guó)打工协饲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缴川。 一個(gè)月前我還...
    沈念sama閱讀 46,571評(píng)論 2 362
  • 正文 我出身青樓茉稠,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親把夸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子而线,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,666評(píng)論 2 350

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