SOFAArk启动源码分析

一、背景

首先来个SOFAArk官方介绍:SOFAArk 是一款基于 Java 实现的轻量级类隔离容器,由蚂蚁金服公司开源贡献;主要提供类隔离和应用(模块)动态部署能力;基于 Fat Jar 技术,可以将多个应用(模块)打包成一个自包含可运行的 Fat Jar,应用既可以是简单的单模块 Java 应用也可以是 Spring Boot/SOFABoot 应用;访问网址进入快速开始并获取更多详细信息;

所以SOFAArk其实是提供一个类隔离能力,那么具体可以解决什么问题?举个栗子,我们知道springboot引用的jar包都放在同一个pom.xml文件中。当工程比较小的时候,一切ok,但如果工程是由多个业务方和团队协作开发时,就会带来不少风险和工作量。比如工程中有两个独立的功能FunA和FunB都依赖了C这个jar包,现在FunA需要升级,那么就需要排查FunB是否兼容,不兼容的代码改动就带来了额外的风险,拔出萝卜带出泥,甚至涉及long long ago的祖传代码。。。

image

SOFAArk不仅仅可以解决这么一个问题,还有包冲突、动态装载卸载模块、合并部署等。但总体来说核心就是类隔离。

二、术语

SOFAArk框架中最重要的三个术语Ark Container, Ark Plugin 和 Ark Biz,看下官方的图示。简单来说SOFAArk框架运行时分为三层:

底层是容器,干啥的呢?就是启动并加载所有plugin和biz模块,然后将他们都部署起来,它会执行一个pipeline按顺序先部署plugin,然后部署biz模块。

中间层是插件,一般是可以共享的中间件和通用服务,例如常见的消息中间件、rpc等,如果你需要提供一个给各个上层业务方使用的通用能力,可以定义在这一层。

最上层就是biz模块层,就是具体的业务代码了。

另外提一点,biz层可以使用plugin的所有导出类,但plugin不能依赖biz层的类。其实原因也很好理解,因为biz的模块是支持动态加载和卸载的,如果plugin对biz有依赖,那么biz一次动态卸载和加载,依赖的类就混乱了。

image

三、类加载器模型

首先SOFAArk容器通过监听springboot启动事件来启动自己,启动类SofaArkBootstrap是由AppClassLoader加载的,然后会创建一个新的ContainerClassLoader来加载容器相关的类。容器启动过程中会为每一个plugin和biz模块都创建一个对应的PluginClassLoader和BizClassLoader,注意是每一个plugin都有一个独立的PluginClassLoader加载,biz模块也一样。这样就做到了类隔离。

因此SOFAArk启动后,会有一个ContainerClassLoader,若干个PluginClassLoader和BizClassLoader。

image

四、启动分析

4.1 源码分析

4.1.1 通过监听springboot的启动事件,来启动ark容器

public void onApplicationEvent(SpringApplicationEvent event) {
    。。。
    startUpArk(event);
    。。。
}

public void startUpArk(SpringApplicationEvent event) {
    if (LAUNCH_CLASSLOADER_NAME.equals(this.getClass().getClassLoader().getClass().getName())) {
        // zy 1 启动sofaark
        SofaArkBootstrap.launch(event.getArgs());
    }
}

4.1.2 SofaArkBootstrap的launch方法通过一个单线程启动容器

public static void launch(String[]args) {
try {
    if (!isSofaArkStarted()) {
        entryMethod =new EntryMethod(Thread.currentThread());
        IsolatedThreadGroup threadGroup =new IsolatedThreadGroup(
        entryMethod.getDeclaringClassName());
        // zy 2 启动的runner方法, MAIN_ENTRY_NAME = "remain"
        LaunchRunner launchRunner =new LaunchRunner(SofaArkBootstrap.class.getName(),
MAIN_ENTRY_NAME,args);
        Thread launchThread =new Thread(threadGroup, launchRunner, entryMethod.getMethodName());
        // zy 3 单线程启动sofaark
        launchThread.start();
        LaunchRunner.join(threadGroup);
        threadGroup.rethrowUncaughtException();
        System.exit(0);
    }
}catch (Throwable e) {
    throw new RuntimeException(e);
}
}

4.1.3 线程里面调用了这个LaunchRunner.run方法,当前这个类LaunchRunner由AppClassLoader加载生成。代码中注释可以看到最终又去执行了SofaArkBootstrap.remain方法

public void run() {
    。。。
    // zy 4 sun.misc.Launcher$AppClassLoader
    ClassLoader classLoader = thread.getContextClassLoader();
    try {
        Class startClass = classLoader.loadClass(this.startClassName);
        Method entryMethod;
        try {
            entryMethod = startClass.getMethod(startMethodName,String[].class);
        }catch (NoSuchMethodException ex) {
        entryMethod = startClass.getDeclaredMethod(startMethodName,String[].class);
    }
    if (!entryMethod.isAccessible()) {
    entryMethod.setAccessible(true);
    }
    // zy 5 执行SofaArkBootstrap.remain方法
    entryMethod.invoke(null,new Object[] {this.args });
    。。。
}

4.1.4 回到SofaArkBootstrap类的remain方法,在第7步执行了ClasspathLauncher.launch方法

    private static void remain(String[] args) throws Exception {// NOPMD
        AssertUtils.assertNotNull(entryMethod, "No Entry Method Found.");
        // zy 6 系统环境变量目录
        URL[] urls = getURLClassPath();
        // zy 7 lunch启动sofaark
        new ClasspathLauncher(new ClassPathArchive(entryMethod.getDeclaringClassName(),
            entryMethod.getMethodName(), urls)).launch(args, getClasspath(urls),
            entryMethod.getMethod());
    }

4.1.5 下面代码再AbstractLauncher.launch方法的第8步,创建了一个ContainerClassLoader类加载器,后面会用来加载SOFAArk相关的类

    // zy 参数method是SofaArkBootstrap.remain方法
    public Object launch(String[] args, String classpath, Method method) throws Exception {
        。。。
        // zy 8 ContainerClassLoader的父cls是null
        ClassLoader classLoader = createContainerClassLoader(getContainerArchive());
        。。。
        return launch(attachArgs.toArray(new String[attachArgs.size()]), getMainClass(),
            classLoader);
    }

上面代码调用了下面的第二个launch方法,最终createMainMethodRunner方法返回的是MainMethodRunner这个工具类的实例,MainMethodRunner只是一个执行器,它实际是为了执行ArkContainer类的main方法。

    /** zy
     * mainClass是com.alipay.sofa.ark.container.ArkContainer
     * classLoader是ContainerClassLoader
     */
    protected Object launch(String[] args, String mainClass, ClassLoader classLoader)
                                                                                     throws Exception {
        // zy 9 old是sun.misc.Launcher$AppClassLoader
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            // zy 10 classLoader是ContainerClassLoader, 其父cls是null
            Thread.currentThread().setContextClassLoader(classLoader);
            return createMainMethodRunner(mainClass, args).run();
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

    protected MainMethodRunner createMainMethodRunner(String mainClass, String[] args) {
        return new MainMethodRunner(mainClass, args);
    }

我们看下MainMethodRunner类的代码,类里面我已经分场景注释了,也就是后面的代码通过这个工具类最终调用了ArkContainer.main()

    /** zy com.alipay.sofa.ark.container.ArkContainer */
    private final String   mainClassName;
    private final String[] args;

    public MainMethodRunner(String mainClass, String[] args) {
        this.mainClassName = mainClass;
        this.args = (args == null ? null : args.clone());
    }

    public Object run() throws Exception {
        /** zy
         * arkContainer启动时, mainClass是ContainerClassLoader
         * biz module启动时, mainClass是BizClassLoader
         */
        Class<?> mainClass = Thread.currentThread().getContextClassLoader()
            .loadClass(this.mainClassName);
        Method mainMethod = mainClass.getDeclaredMethod("main", String[].class);
        /** zy
         * arkContainer启动时, 执行com.alipay.sofa.ark.container.ArkContainer.main
         * biz module启动时, 执行模块里面的main方法
         */
        return mainMethod.invoke(null, new Object[] { this.args });
    }

4.1.6 回到AbstractLauncher的第二个launch方法,可以看到,在第11步调用了ArkContainer.main方法

    /** zy
     * mainClass是com.alipay.sofa.ark.container.ArkContainer
     * classLoader是ContainerClassLoader
     */
    protected Object launch(String[] args, String mainClass, ClassLoader classLoader)
                                                                                     throws Exception {
        // zy 9 old是sun.misc.Launcher$AppClassLoader
        ClassLoader old = Thread.currentThread().getContextClassLoader();
        try {
            // zy 10 classLoader是ContainerClassLoader, 其父cls是null
            Thread.currentThread().setContextClassLoader(classLoader);
            // zy 11 执行ArkContainer.main
            return createMainMethodRunner(mainClass, args).run();
        } finally {
            Thread.currentThread().setContextClassLoader(old);
        }
    }

4.1.7 进入ArkContainer.main方法,在第13步执行启动容器代码

    /** zy 当前类属于ContainerClassLoader, 其父cls是null */
    public static Object main(String[] args) throws ArkRuntimeException {
        if (args.length < MINIMUM_ARGS_SIZE) {
            throw new ArkRuntimeException("Please provide suitable arguments to continue !");
        }
        。。。
        // zy 12 解析bizJar,className,methodName,classpath,profile等参数
        LaunchCommand launchCommand = LaunchCommand.parse(args);
        if (launchCommand.isExecutedByCommandLine()) {
            。。。
            // zy 13 启动容器; 当前属于ContainerClassLoader, 其父cls是null
            return new ArkContainer(executableArchive, launchCommand).start();
        }
        。。。
    }

4.1.8 进入ArkContainer.start方法,依次添加了jvm钩子、初始化配置、初始化log配置、启动ArkServiceContainer以及执行Pipeline。接下来依次详解ArkServiceContainer启动流程和Pipeline执行流程

    public Object start() throws ArkRuntimeException {
        。。。
        if (started.compareAndSet(false, true)) {
            // zy 14 JVM关闭时, 优雅的关闭arkContainer
            Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
                @Override
                public void run() {
                    stop();
                }
            }));
            // zy 15 读取bootstrap.properties以及bootstrap-%s.properties中的属性, 并放入到ArkConfigs.CFG
            prepareArkConfig();
            // zy 16 初始化log相关配置
            reInitializeArkLogger();
            // zy 17 启动Ark Service Container
            arkServiceContainer.start();
            // zy 18 StandardPipeline
            Pipeline pipeline = arkServiceContainer.getService(Pipeline.class);
            // zy 19 执行pipeline
            pipeline.process(pipelineContext);
            。。。
    }

4.1.9 首先进入ArkServiceContainer.start方法,在17.2步注册了pipeline相关的服务,哪些服务后面会说到,另外这里使用的是Guice框架。在17.3步,注册了biz模块管理,注入,异步事件服务。

    public void start() throws ArkRuntimeException {
        if (started.compareAndSet(false, true)) {
                。。。
                // zy 17.2 这里Modules值是 com.alipay.sofa.ark.container.guice.ContainerModule
                injector = Guice.createInjector(findServiceModules());
                。。。

                /** zy 17.3 其中arkServiceList包含如下服务:
                 *     @see PluginDeployServiceImpl    空执行
                 *     @see BizDeployServiceImpl       空执行
                 *     @see ClassLoaderServiceImpl     生成arkClassLoader,systemClassLoader等
                 *     @see StandardTelnetServerImpl   开启telnet服务
                 */
                for (ArkService arkService : arkServiceList) {
                    arkService.init();
                }

                // zy 17.4 注册biz模块管理,注入,异步事件服务
                ArkServiceContainerHolder.setContainer(this);
                ArkClient.setBizFactoryService(getService(BizFactoryService.class));
                ArkClient.setBizManagerService(getService(BizManagerService.class));
                ArkClient.setInjectionService(getService(InjectionService.class));
                ArkClient.setEventAdminService(getService(EventAdminService.class));
                ArkClient.setArguments(arguments);
                。。。
        }
    }

4.1.10 上面提到的findServiceModules代码最终注入的是下面这个模型,下面可以清楚的看到添加了插件、biz模块、扩展点、事件等服务。这些会在pipeline执行以及容器运行期会用到。

public class ContainerModule extends AbstractArkGuiceModule {

    @Override
    protected void configure() {
        binder().bind(Pipeline.class).to(StandardPipeline.class);

        Multibinder<ArkService> arkServiceMultibinder = Multibinder.newSetBinder(binder(),
            ArkService.class);
        arkServiceMultibinder.addBinding().to(PluginDeployServiceImpl.class);
        arkServiceMultibinder.addBinding().to(BizDeployServiceImpl.class);
        arkServiceMultibinder.addBinding().to(ClassLoaderServiceImpl.class);
        arkServiceMultibinder.addBinding().to(StandardTelnetServerImpl.class);

        binder().bind(PluginManagerService.class).to(PluginManagerServiceImpl.class);
        binder().bind(BizManagerService.class).to(BizManagerServiceImpl.class);
        binder().bind(ClassLoaderService.class).to(ClassLoaderServiceImpl.class);
        binder().bind(PluginDeployService.class).to(PluginDeployServiceImpl.class);
        binder().bind(BizDeployService.class).to(BizDeployServiceImpl.class);
        binder().bind(RegistryService.class).to(RegistryServiceImpl.class);
        binder().bind(InjectionService.class).to(InjectionServiceImpl.class);
        binder().bind(TelnetServerService.class).to(StandardTelnetServerImpl.class);
        binder().bind(BizFactoryService.class).to(BizFactoryServiceImpl.class);
        binder().bind(PluginFactoryService.class).to(PluginFactoryServiceImpl.class);
        binder().bind(ExtensionLoaderService.class).to(ExtensionLoaderServiceImpl.class);
        binder().bind(EventAdminService.class).to(EventAdminServiceImpl.class);
    }
}

4.1.11 接下来我们进入pipeline的代码StandardPipeline.process,执行代码很简单,注释上也说明了执行了哪些Stage以及他们的功能。

    public void process(PipelineContext pipelineContext) throws ArkRuntimeException {
        for (PipelineStage pipelineStage : stages) {
                。。。
                /**
                 * zy
                 *
                 * @see HandleArchiveStage   注册plugin和biz模块, 仅注册不部署
                 * @see RegisterServiceStage plugin,模块和事件这些内部服务不可被覆盖, 因此先发布
                 * @see ExtensionLoaderStage 扩展服务
                 * @see DeployPluginStage    部署插件
                 * @see DeployBizStage       部署biz模块
                 * @see FinishStartupStage   结束, 里面主要发送一个结束事件
                 *
                 */
                pipelineStage.process(pipelineContext);
                。。。
        }
    }

4.1.12 我们分析下DeployPluginStage的代码,第一步是解析导入导出相关的类、加载器等资源,第二步是部署插件。

    public void process(PipelineContext pipelineContext) throws ArkRuntimeException {
        // zy 解析并缓存, 插件中导入导出的类,类加载和资源等
        classloaderService.prepareExportClassAndResourceCache();
        // zy 部署插件
        pluginDeployService.deploy();
    }

4.1.13 首先我们看下,第一步解析相关的代码,可以看到整个代码逻辑是比较清晰的,就是根据不同的类型进行解析。

    public void prepareExportClassAndResourceCache() {
        for (Plugin plugin : pluginManagerService.getPluginsInOrder()) {
            for (String exportIndex : plugin.getExportPackageNodes()) {
                exportNodeAndClassLoaderMap.putIfAbsent(exportIndex, plugin.getPluginClassLoader());
            }
            for (String exportIndex : plugin.getExportPackageStems()) {
                exportStemAndClassLoaderMap.putIfAbsent(exportIndex, plugin.getPluginClassLoader());
            }
            for (String exportIndex : plugin.getExportClasses()) {
                exportClassAndClassLoaderMap
                    .putIfAbsent(exportIndex, plugin.getPluginClassLoader());
            }
            for (String resource : plugin.getExportResources()) {
                exportResourceAndClassLoaderMap.putIfAbsent(resource, new LinkedList<>());
                exportResourceAndClassLoaderMap.get(resource).add(plugin.getPluginClassLoader());
            }
            for (String resource : plugin.getExportPrefixResourceStems()) {
                exportPrefixStemResourceAndClassLoaderMap.putIfAbsent(resource, new LinkedList<>());
                exportPrefixStemResourceAndClassLoaderMap.get(resource).add(
                    plugin.getPluginClassLoader());
            }
            for (String resource : plugin.getExportSuffixResourceStems()) {
                exportSuffixStemResourceAndClassLoaderMap.putIfAbsent(resource, new LinkedList<>());
                exportSuffixStemResourceAndClassLoaderMap.get(resource).add(
                    plugin.getPluginClassLoader());
            }
        }
    }

4.1.14 然后我们看下,第二步插件部署逻辑PluginDeployServiceImpl.deploy代码,按照顺序,循环部署。

    public void deploy() throws ArkRuntimeException {
        for (Plugin plugin : pluginManagerService.getPluginsInOrder()) {
            。。。
            deployPlugin(plugin);
            。。。
        }
    }

    private void deployPlugin(Plugin plugin) throws ArkRuntimeException {
        。。。
        plugin.start();
        。。。
    }

4.1.15 继续往下看,在PluginModel.start方法中,整个代码逻辑是用插件自己的类加载器PluginClassLoader加载了插件中实现的启动类PluginActivator,最后通过执行pluginActivator.start来启动插件。

    public void start() throws ArkRuntimeException {
        。。。
        EventAdminService eventAdminService = ArkServiceContainerHolder.getContainer().getService(
            EventAdminService.class);

        // zy 每个插件都有一个自己的类加载器: PluginClassLoader
        ClassLoader oldClassLoader = ClassLoaderUtils
            .pushContextClassLoader(this.pluginClassLoader);
        try {
            eventAdminService.sendEvent(new BeforePluginStartupEvent(this));
            // zy 使用自己的PluginClassLoader加载插件的启动类
            pluginActivator = (PluginActivator) pluginClassLoader.loadClass(activator)
                .newInstance();
            // zy 调用start方法, 启动插件
            pluginActivator.start(pluginContext);
        } catch (Throwable ex) {
            throw new ArkRuntimeException(ex.getMessage(), ex);
        } finally {
            eventAdminService.sendEvent(new AfterPluginStartupEvent(this));
            ClassLoaderUtils.popContextClassLoader(oldClassLoader);

        }
    }

4.1.16 至此插件的启动依然清晰,4.8.3中剩下biz模块也比较重要,我们再继续分析下,进入到DeployBizStage代码,不同于插件部署,biz模块部署后会通过eventAdminService.sendEvent发送一个事件,用于通知相关的事件钩子。

    public void process(PipelineContext pipelineContext) throws ArkRuntimeException {
        String[] args = pipelineContext.getLaunchCommand().getLaunchArgs();
        bizDeployService.deploy(args);
        eventAdminService.sendEvent(new AfterFinishDeployEvent());
    }

4.1.17 继续看bizDeployService.deploy的代码,init方法就是给变量赋值了,没什么。继续跟bizDeployer.deploy的代码

    public void deploy(String[] args) throws ArkRuntimeException {
        ServiceReference<BizDeployer> serviceReference = registryService
            .referenceService(BizDeployer.class);
        bizDeployer = serviceReference.getService();
        。。。。
        bizDeployer.init(args);
        bizDeployer.deploy();
    }

4.1.18 进入bizDeployer.deploy的代码,可以看到和插件部署类似,也是按照顺序,循环启动biz模块。

    public void deploy() {
        for (Biz biz : bizManagerService.getBizInOrder()) {
            。。。
            biz.start(arguments);
            。。。
        }
    }

4.1.19 进入BizModel.start方法,依然使用了MainMethodRunner这个工具类来启动当前biz模块,参数mainClass是biz模块中有main方法的启动类,是在创建BizModel对象的时候设置的,jar包的解析流程,后面我会再写一篇文章分析下。这里和插件启动一样,每个biz模块也会由一个独立的BizClassLoader负责加载模块所有的类。

    public void start(String[] args) throws Throwable {
        。。。
        // zy 替换成BizClassLoader
        ClassLoader oldClassLoader = ClassLoaderUtils.pushContextClassLoader(this.classLoader);
        EventAdminService eventAdminService = ArkServiceContainerHolder.getContainer().getService(
            EventAdminService.class);
        。。。
            eventAdminService.sendEvent(new BeforeBizStartupEvent(this));
            resetProperties();
            MainMethodRunner mainMethodRunner = new MainMethodRunner(mainClass, args);
            // zy 部署biz模块
            mainMethodRunner.run();
            // this can trigger health checker handler
            eventAdminService.sendEvent(new AfterBizStartupEvent(this));
        。。。
    }

4.2 启动流程回顾

从下面的类图中可以总结出启动过程中最重要的三个类:SofaArkBootstrap、ArkContainer以及ArkServiceContainer,插件和biz模块都是在ArkServiceContainer中通过pipeline机制,部署起来的。


image.png

4.3 类加载

从上面的代码分析可以看到,SOFAArk里面的ContainerClassLoader、PluginClassLoader以及BizClassLoader,都是打破了双亲委派机制,采用了一种分发机制去加载类。
以BizClassLoader为例,如下图所示,会根据类的类型,使用不同的方式加载类。所以SOFAArk里面的类加载是一种分发加载机制。


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343