从Spring Boot2.0启动流程看自动装配EnableAutoConfiguration

本文将从Spring Boot 2.0的启动流程来解析其中的一些关键内容,本文源码的版本为spring-boot-starter-parent 2.4.6,不熟悉spring源码的建议先熟悉下spring源码,话不多说上代码(女朋友之前吐槽很讨厌上来直接就写源码的博客,后面尽量总结下流程和知识点)。

  public static void main(String[] args) {
        SpringApplication.run(MergePayApplication.class, args);
    }

以上的代码大家可能最熟悉不过了,这也是我们了解Spring Boot原理的入口,小伙伴最好可以跟着代码进行调试来加深印象(其实是检查写的内容是否有错误)。

一、SpringApplication

1.构造函数
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        *this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
        *setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
        *setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }
  • resourceLoader 调用传入的为null,暂时可以忽略;
  • primarySources 传入的为run的第一个参数,本例子为MergePayApplication;
  • webApplicationType 该方法会返回web应用的三种类型分别为REACTIVE、SERVLET、NONE,本例子返回的为SERVLET;
  • bootstrapRegistryInitializers、initializers、listeners这部分也是该方法的重点内容,带*的三行分别设置了三个List类型的变量;
  • mainApplicationClass启动类本例子返回MergePayApplication类;
private List<BootstrapRegistryInitializer> getBootstrapRegistryInitializersFromSpringFactories() {
        ArrayList<BootstrapRegistryInitializer> initializers = new ArrayList<>();
        getSpringFactoriesInstances(Bootstrapper.class).stream()
                .map((bootstrapper) -> ((BootstrapRegistryInitializer) bootstrapper::initialize))
                .forEach(initializers::add);
        initializers.addAll(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
        return initializers;
    }

其实有的时候看源码不用看到每一行都想去弄明白,之前我也是这么看代码,但是到最后根本记不住大体的流程,满脑子都是一些无关紧要的细节,就像上面代码的BootstrapRegistryInitializer初看不知道是什么鬼,但是接着看它后面的赋值和调用猜个八九不离十。

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

终于到了重点的方法,该方法主要分为两个不步骤,一个是SpringFactoriesLoader.loadFactoryNames,一个是createSpringFactoriesInstances;

2.loadFactoryNames

在loadFactoryNames方法中主要是通过SpringFactoriesLoader#loadSpringFactories方法读取classpath下所有jar中META-INF文件夹下的spring.factories文件,以spring-boot-autoconfigure-2.4.6为例,实例如下第一\前的为实现的接口名称,\后面的内容为需要加载的类,一般我们在Spring Boot进行一些扩展到时候都是使用到EnableAutoConfiguration,大家也可以将这部分的实现理解为我们自定义一个starter包(如 mybatis-spring-boot-starter),我们就需要将自己实现的功能(如MybatisAutoConfiguration)与Spring Boot进行融合从而实现自动配置。

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration

在该方法中将META-INF/spring.factories文件转化为urls,最后将spring.factories中的对应关系存储在result中,并在cache中进行缓存,整个流程只需要加载一次这个过程即可。

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        //static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
        Map<String, List<String>> result = cache.get(classLoader);
        if (result != null) {
            return result;
        }

        result = new HashMap<>();
        try {
            Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            while (urls.hasMoreElements()) {
                URL url = urls.nextElement();
                UrlResource resource = new UrlResource(url);
                Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                for (Map.Entry<?, ?> entry : properties.entrySet()) {
                    String factoryTypeName = ((String) entry.getKey()).trim();
                    String[] factoryImplementationNames =
                            StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                    for (String factoryImplementationName : factoryImplementationNames) {
                        result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                                .add(factoryImplementationName.trim());
                    }
                }
            }

            // Replace all lists with unmodifiable lists containing unique elements
            result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
                    .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
            cache.put(classLoader, result);
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load factories from location [" +
                    FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
        return result;
    }

result中存储的<key,value>结构<key,value>如下,上层的调用方法会根据,并根据BootstrapRegistryInitializer、ApplicationContextInitializer、ApplicationListener(可以提前看看源码的注释呦)三种类型从result中返回不同的三种集合初始化到刚刚说的bootstrapRegistryInitializers、initializers、listeners三个变量中。

  • BootstrapRegistryInitializer:对BootstrapRegistry的回调,可以注册一些创建成本高或者在ApplicationContext之前的共享变量;
  • ApplicationContextInitializer:用于在spring容器刷新之前初始化Spring,ConfigurableApplicationContext的回调接口,在容器刷新之前调用该类的 initialize 方法。
  • ApplicationListener:熟悉Spring源码的应该知道这是一个监听者模式,作为一种回调,在spring上下文创建完成后进行统一的调用。
3.createSpringFactoriesInstances

这部分比较简单就不贴源码了(要不女朋友又要骂我了),主要就是根据result中筛选出的类利用反射及逆行初始化,然后应用AnnotationAwareOrderComparator根据注解(@Order、@Priority)进行排序。

截至到现在SpringApplication的初始化就介绍完啦,是不是很简单呀,如果还不是特别的了解,可以看下我的小伙伴的提纲博客加深印象,下面我们介绍下启动流程吧。

二、Run启动流程

Spring Boot的启动流程可以浓缩成这一个方法,我们的介绍也是从这个方法进行,大家不要慌,慢慢看应该可以看得懂的。

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        DefaultBootstrapContext bootstrapContext = createBootstrapContext();
        ConfigurableApplicationContext context = null;
        configureHeadlessProperty();
        SpringApplicationRunListeners listeners = getRunListeners(args);
        listeners.starting(bootstrapContext, this.mainApplicationClass);
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
            configureIgnoreBeanInfo(environment);
            Banner printedBanner = printBanner(environment);
            context = createApplicationContext();
            context.setApplicationStartup(this.applicationStartup);
            prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
            refreshContext(context);
            afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
            }
            listeners.started(context);
            callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, listeners);
            throw new IllegalStateException(ex);
        }

        try {
            listeners.running(context);
        }
        catch (Throwable ex) {
            handleRunFailure(context, ex, null);
            throw new IllegalStateException(ex);
        }
        return context;
    }
  • StopWatch是一个计时器的封装,通过start和stop方法来计算运行的时间;
  • createBootstrapContext将第一部分中bootstrapRegistryInitializers保存的BootstrapRegistryInitializer实例取出来逐一的调用initialize方法进行执行,参数为BootstrapRegistry;
  • configureHeadlessProperty设置Headless(java.awt.headless)的属性状态,这个可以忽略不用较真儿去理解Headless;
  • getRunListeners从第一部分我们说的cache中找到SpringApplicationRunListener的实现类,默认只有只有一个EventPublishingRunListener,并将它作为参数(listeners)初始化SpringApplicationRunListeners,SpringApplicationRunListener的作用是作为SpringApplication的run方法的监听器。通过代码中的listeners.starting方法将starting的Event发布到Spring的所有ApplicationListener监听器中。
1.EventPublishingRunListener

看到这大家应该有点晕了吧,一堆Listener绕来绕去,写到这我自己差点都晕了,下面针对在这些Listener进行以下简单的梳理。


该部分是通过doWithListeners方法将spring.boot.application.starting事件ApplicationStartingEvent通过Spring的SimpleApplicationEventMulticaster发布到Spring的事件传播器中,在multicastEvent方法中getApplicationListeners会筛选与ApplicationStartingEvent匹配的ApplicationListeners,然后通过invokeListener执行onApplicationEvent方法实现starting事件的传播。

    void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
        doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
                (step) -> {
                    if (mainApplicationClass != null) {
                        step.tag("mainApplicationClass", mainApplicationClass.getName());
                    }
                });
    }
@Override
    public void starting(ConfigurableBootstrapContext bootstrapContext) {
        //通过initialMulticaster实现与Spring事件发布的融合
        this.initialMulticaster
                .multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
    }
public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
        ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);  
        Executor executor = this.getTaskExecutor();
        //得到与发布的event的一致的listener
        Iterator var5 = this.getApplicationListeners(event, type).iterator();

        while(var5.hasNext()) {
            ApplicationListener<?> listener = (ApplicationListener)var5.next();
            if (executor != null) {
                executor.execute(() -> {
                    this.invokeListener(listener, event);
                });
            } else {
                //listener回调参数为发布的event
                this.invokeListener(listener, event);
            }
        }

    }
2.prepareEnvironment

然后我们接着回到run方法中继续执行第一try的代码中,DefaultApplicationArguments主要是讲args进行封装,args这个参数之前一直没有介绍,其实就是启动的参数入在jar -jar启动项目包的时候制定的一些参数,如--server.port等。

紧接着是prepareEnvironment方法,这个方法看着其实比较头痛,其实要是给每一个方法都讲清楚和明白,其实和写源码的注释就差不多了,这个文章给关键方法的着重讲解下, 其他的就简单说一下用处。

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
            DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
        // Create and configure the environment
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        ConfigurationPropertySources.attach(environment);
        listeners.environmentPrepared(bootstrapContext, environment);
        DefaultPropertiesPropertySource.moveToEnd(environment);
        bindToSpringApplication(environment);
        if (!this.isCustomEnvironment) {
            environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
                    deduceEnvironmentClass());
        }
        ConfigurationPropertySources.attach(environment);
        return environment;
    }
  • getOrCreateEnvironment方法会根据我们之前在初始化SpringApplicaiton时候的
    webApplicationType类型返回ConfigurableEnvironment的实现类,本例子中是Servlet所有返回StandardServletEnvironment,响应式返回StandardReactiveWebEnvironment,其他返回StandardEnvironment;

  • configureEnvironment方法,第一步是配置一个ConversionService,大家就理解为是一个转换工具就可以,configurePropertySources方法会将启动参数的配置封装到SimpleCommandLinePropertySource中,最终加入到Environment中。configureProfiles方法在本例子中该方法中没有任何代码,大家先忽略;
    大家可以看一下environment这个对象,大家可以简单的理解就是所有的配置按照不同的优先级和实例组织在propertySources中。


  • ConfigurationPropertySources.attach方法是将configurationProperties添加到propertySources中;

  • listeners.environmentPrepared方法,大家看到listeners应该就懂了,和前面的listeners执行starting类似,回调的参数是DefaultBootstrapContext和Environment;在这个方法中主要是处理一些SpringBoot的一些配置,其中有一个比较重要的EnvironmentPostProcessorApplicationListener,他是处理实现所有实现EnvironmentPostProcessor接口的类,有点像Spring中BeanPostProcessor的作用,其中一个ConfigDataEnvironmentPostProcessor实现了读取我们properties配置文件并加入到environment中的作用。

EnvironmentPostProcessor 这多说一句如果在SpringBoot的应用程序中,如果想对配置进行一些修改就可以实现这个接口然后进行自定义的扩展,这部分在我们的开发中也有使用过,如不通过配置在项目启动的时候添加一些后面会使用的参数 。

  • DefaultPropertiesPropertySource.moveToEnd方法,本例子中没有defaultProperties配置,所以没有执行;
  • bindToSpringApplication实现environment和SpringApplication的绑定,new EnvironmentConverter方法是对Environment通过进行一些转换;

至此prepareEnvironment方法已经介绍完了是不是很好理解,其实有时候看源码也不用说每一行都看懂在干什么,知道大概的流程就可以了,那回来头我们接着说我们的run方法。

  • configureIgnoreBeanInfo方法,将environment中的spring.beaninfo.ignore属性设置到System环境变量中,这部分也可以不关注;

  • printBanner方法,打印SpringBoot的启动Logo愿意改可以自定义。

  • createApplicationContext方法,根据SpirngApplication中的webApplicationType类型来返回ApplicaitonContext(简称上下文),本例子是一个Servlet项目,返回AnnotationConfigServletWebServerApplicationContext,响应式返回AnnotationConfigReactiveWebServerApplicationContext,其他返回AnnotationConfigApplicationContext;AnnotationConfigServletWebServerApplicationContext里面就我们很熟悉的两个类,一个是AnnotatedBeanDefinitionReader,一个是ClassPathBeanDefinitionScanner,(不太清楚的孩子去回忆Spring哈);

  • context.setApplicationStartup,将上下文关联applicationStartup;

3.prepareContext

这个方法是上下文的准备工作,看参数的个数大家应该就知道这个类比较重要,参数基本上包括了上述的大部分内容。

    private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);
        postProcessApplicationContext(context);
        applyInitializers(context);
        listeners.contextPrepared(context);
        bootstrapContext.close(context);
        if (this.logStartupInfo) {
            logStartupInfo(context.getParent() == null);
            logStartupProfileInfo(context);
        }
        // Add boot specific singleton beans
        **ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
        if (printedBanner != null) {
            beanFactory.registerSingleton("springBootBanner", printedBanner);
        }
        if (beanFactory instanceof DefaultListableBeanFactory) {
            ((DefaultListableBeanFactory) beanFactory)
                    .setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }
        if (this.lazyInitialization) {
            context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
        }**
        // Load the sources
        Set<Object> sources = getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        load(context, sources.toArray(new Object[0]));
        listeners.contextLoaded(context);
    }
  • context.setEnvironment上线文关联environment;
  • postProcessApplicationContext在本例子中只将ApplicationConversionService进行关联;
  • applyInitializers方法是将在SpringApplicaiton构造的时候初始化的initializer循环执行其中的initialize方法。
  • listeners.contextPrepared这个应该也不用多说了和前面starting的流程类似;
  • bootstrapContext.close关闭启动器;
  • 上面代码中两个**中的内容其实就是将上线中的BeanFactory去出来然后进行一些信息的注册或设置,如springApplicationArguments、springBootBanner等;
  • load方法是将启动类Source作为参数然后将bean加载到Spring上下文中,过程是通过AnnotatedBeanDefinitionReader类对启动类进行Bean定义的注册;
  • listeners.contextLoaded最后执行contextLoaded方法,此部分与starting部分也类似;
4.refreshContext

refreshContext中有两部分工作,一部分是在Runtime中注册一个钩子(可以理解为一个线程),当程序执行完成后会做一个操作,SpringBoot写的钩子大体上是完成一写资源的销毁。另一部分是核心,也是SpringBoot与Spring的连接点,在方法的最后执行了Spring的applicationContext的refresh方法,然后完成Spring上下文中所有Bean的初始化;

    private void refreshContext(ConfigurableApplicationContext context) {
        if (this.registerShutdownHook) {
            try {
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
        refresh((ApplicationContext) context);
    }

截至现在run方法中我们还有2行比较重要的代码就结束了,listeners.started就不多说了,另一个是callRunners,它的主要作用是SpringBoot应用启动完成后进行一个回调,可以实现ApplicationRunner接口也可以实现CommandLineRunner接口,代码如下。

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<>();
        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
        runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
        AnnotationAwareOrderComparator.sort(runners);
        for (Object runner : new LinkedHashSet<>(runners)) {
            if (runner instanceof ApplicationRunner) {
                callRunner((ApplicationRunner) runner, args);
            }
            if (runner instanceof CommandLineRunner) {
                callRunner((CommandLineRunner) runner, args);
            }
        }
    }

主要的流程是得到实现两个接口的类然后按照@Order注解的顺序进行排序,他们两个接口的区别就是回调的参数不同,一个是ApplicationArguments,一个是String... args,然后循环执行各自的run方法的进行回调。还有一个小细节就是在执行的过程中如果有异常依旧会调用listeners.failed方法。

三、@EnableAutoConfiguration注解

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

推荐阅读更多精彩内容