SpringBoot源碼分析之配置環(huán)境的構造過程

SpringBoot把配置文件的加載封裝成了PropertySourceLoader接口危尿,該接口的定義如下:

public interface PropertySourceLoader {
    // 支持的文件后綴
    String[] getFileExtensions();
    // 把資源Resource加載成屬性源PropertySource
    PropertySource<?> load(String name, Resource resource, String profile)
            throws IOException;
}

PropertySource是Spring對name/value鍵值對的封裝接口霞揉。該定義了getSource()方法妙啃,這個方法會返回得到屬性源的源頭芬失。比如MapPropertySource的源頭就是一個Map们衙,PropertiesPropertySource的源頭就是一個Properties剃斧。

PropertySource目前的實現類有不少,比如上面提到的MapPropertySource和PropertiesPropertySource显歧,還有RandomValuePropertySource(source是Random)仪或、SimpleCommandLinePropertySource(source是CommandLineArgs,命令行參數)士骤、ServletConfigPropertySource(source是ServletConfig)等等范删。

PropertySourceLoader接口目前有兩個實現類:PropertiesPropertySourceLoader和YamlPropertySourceLoader。

PropertiesPropertySourceLoader支持從xml或properties格式的文件中加載數據敦间。

YamlPropertySourceLoader支持從yml或者yaml格式的文件中加載數據瓶逃。

Environment的構造以及PropertySource的生成

Environment接口是Spring對當前程序運行期間的環(huán)境的封裝束铭。主要提供了兩大功能:profile和property(父接口PropertyResolver提供)廓块。目前主要有StandardEnvironment、StandardServletEnvironment和MockEnvironment3種實現契沫,分別代表普通程序带猴、Web程序以及測試程序的環(huán)境。

下面這段代碼就是SpringBoot的run方法內調用的懈万,它會在Spring容器構造之前調用拴清,創(chuàng)建環(huán)境信息:

// SpringApplication.class
private ConfigurableApplicationContext createAndRefreshContext(
        SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments) {
    ConfigurableApplicationContext context;
    // 如果是web環(huán)境靶病,創(chuàng)建StandardServletEnvironment
    // 否則,創(chuàng)建StandardEnvironment
    // StandardServletEnvironment繼承自StandardEnvironment口予,StandardEnvironment繼承AbstractEnvironment
    // AbstractEnvironment內部有個MutablePropertySources類型的propertySources屬性娄周,用于存儲多個屬性源PropertySource
    // StandardEnvironment構造的時候會默認加上2個PropertySource。分別是MapPropertySource(調用System.getProperties()配置)和SystemEnvironmentPropertySource(調用System.getenv()配置)
    ConfigurableEnvironment environment = getOrCreateEnvironment();
    // 如果設置了一些啟動參數args沪停,添加基于args的SimpleCommandLinePropertySource
    // 還會配置profile信息煤辨,比如設置了spring.profiles.active啟動參數,設置到環(huán)境信息中
    configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 觸發(fā)ApplicationEnvironmentPreparedEvent事件
    listeners.environmentPrepared(environment);
    ...
}

SpringBoot源碼分析之SpringBoot的啟動過程這篇文章中木张,我們分析過SpringApplication啟動的時候會使用工廠加載機制初始化一些初始化器和監(jiān)聽器众辨。其中org.springframework.boot.context.config.ConfigFileApplicationListener這個監(jiān)聽器會被加載:

    // spring-boot-version.release/META-INF/spring.factories
    org.springframework.context.ApplicationListener=\
    ...
    org.springframework.boot.context.config.ConfigFileApplicationListener,\
    ...

ConfigFileApplicationListener會監(jiān)聽SpringApplication啟動的時候發(fā)生的事件,它的監(jiān)聽代碼:

@Override
public void onApplicationEvent(ApplicationEvent event) {
    // 應用環(huán)境信息準備好的時候對應的事件舷礼。此時Spring容器尚未創(chuàng)建鹃彻,但是環(huán)境已經創(chuàng)建
    if (event instanceof ApplicationEnvironmentPreparedEvent) {
        onApplicationEnvironmentPreparedEvent(
                (ApplicationEnvironmentPreparedEvent) event);
    }
    // Spring容器創(chuàng)建完成并在refresh方法調用之前對應的事件
    if (event instanceof ApplicationPreparedEvent) {
        onApplicationPreparedEvent(event);
    }
}

private void onApplicationEnvironmentPreparedEvent(
        ApplicationEnvironmentPreparedEvent event) {
    // 使用工廠加載機制讀取key為org.springframework.boot.env.EnvironmentPostProcessor的實現類
    List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
    // 加上自己。ConfigFileApplicationListener也是一個EnvironmentPostProcessor接口的實現類
    postProcessors.add(this);
    // 排序
    AnnotationAwareOrderComparator.sort(postProcessors);
    // 遍歷這些EnvironmentPostProcessor妻献,并調用postProcessEnvironment方法
    for (EnvironmentPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessEnvironment(event.getEnvironment(),
                event.getSpringApplication());
    }
}

ConfigFileApplicationListener也是一個EnvironmentPostProcessor接口的實現類蛛株,在這里會被調用:

// ConfigFileApplicationListener的postProcessEnvironment方法
@Override
public void postProcessEnvironment(ConfigurableEnvironment environment,
        SpringApplication application) {
    // 添加屬性源到環(huán)境中
    addPropertySources(environment, application.getResourceLoader());
    // 配置需要ignore的beaninfo
    configureIgnoreBeanInfo(environment);
    // 從環(huán)境中綁定一些參數到SpringApplication中
    bindToSpringApplication(environment, application);
}

protected void addPropertySources(ConfigurableEnvironment environment,
        ResourceLoader resourceLoader) {
    // 添加一個RandomValuePropertySource到環(huán)境中
    // RandomValuePropertySource是一個用于處理隨機數的PropertySource,內部存儲一個Random類的實例
    RandomValuePropertySource.addToEnvironment(environment);
    try {
        // 構造一個內部類Loader育拨,并調用它的load方法
        new Loader(environment, resourceLoader).load();
    }
    catch (IOException ex) {
        throw new IllegalStateException("Unable to load configuration files", ex);
    }
}

內部類Loader的處理過程整理如下:

  1. 創(chuàng)建PropertySourcesLoader泳挥。PropertySourcesLoader內部有2個屬性,分別是PropertySourceLoader集合和MutablePropertySources(內部有PropertySource的集合)至朗。最終加載完畢之后MutablePropertySources屬性中的PropertySource會被添加到環(huán)境Environment中的屬性源列表中屉符。PropertySourcesLoader被構造的時候會使用工廠加載機制獲得PropertySourceLoader集合(默認就2個:PropertiesPropertySourceLoader和YamlPropertySourceLoader;可以自己擴展)锹引,然后設置到屬性中
  2. 獲取環(huán)境信息中激活的profile(啟動項目時設置的spring.profiles.active參數)矗钟。如果沒設置profile,默認使用default這個profile嫌变,并添加到profiles隊列中吨艇。最后會添加一個null到profiles隊列中(為了獲取沒有指定profile的配置文件。比如環(huán)境中有application.yml和appliation-dev.yml腾啥,這個null就保證優(yōu)先加載application.yml文件)
  3. profiles隊列取出profile數據东涡,使用PropertySourcesLoader內部的各個PropertySourceLoader支持的后綴去目錄(默認識別4種目錄classpath:/[類加載目錄],classpath:/config/[類加載目錄下的config目錄],file:./[當前目錄],file:./config/[當前目錄下的config目錄])查找application文件名(這4個目錄是默認的,可以通過啟動參數spring.config.location添加新的目錄倘待,文件名可以通過啟動參數spring.config.name修改)疮跑。比如目錄是file:/,文件名是application凸舵,后綴為properties祖娘,那么就會查找file:/application.properties文件,如果找到啊奄,執(zhí)行第4步
  4. 找出的屬性源文件被加載渐苏,然后添加到PropertySourcesLoader內部的PropertySourceLoader集合中掀潮。如果該屬性源文件中存在spring.profiles.active配置,識別出來并加入第2步中的profiles隊列琼富,然后重復第3步
  5. 第4步找到的屬性源從PropertySourcesLoader中全部添加到環(huán)境信息Environment中仪吧。如果這些屬性源存在defaultProperties配置,那么會添加到Environment中的屬性源集合頭部鞠眉,否則添加到尾部

比如項目中classpath下存在application.yml文件和application-dev.yml邑商,application.yml文件的內容如下:

    spring.profiles.active: dev

直接啟動項目,開始解析凡蚜,過程如下:

  1. 從環(huán)境信息中找出是否設置profile人断,發(fā)現沒有設置。 添加默認的profile - default朝蜘,然后添加到隊列里恶迈,最后添加null的profile。此時profiles隊列中有2個元素:default和null
  2. profiles隊列中先拿出null的profile谱醇。然后遍歷4個目錄和2個PropertySourceLoader中的4個后綴(PropertiesPropertySourceLoader的properties和xml以及YamlPropertySourceLoader的yml和yaml)的application文件名暇仲。file:./config/application.properties、file:./application.properties副渴、classpath:/config/application.properties奈附、classpath:/application.properties、file:./config/application.xml; file:./application.xml ....
  3. 找到classpath:/application.yml文件煮剧,解析成PropertySource并添加到PropertySourcesLoader里的MutablePropertySources中斥滤。由于該文件存在spring.profiles.active配置,把dev添加到profiles隊列中
  4. profiles隊列拿出dev這個profile勉盅。由于存在profile佑颇,尋找文件的時候會帶上profile,重復第3步草娜,比如classpath:/application-dev.yml...
  5. 找到classpath:/application-dev.yml文件挑胸,解析成PropertySource并添加到PropertySourcesLoader里的MutablePropertySources中
  6. profiles隊列拿出default這個profile。尋找文件發(fā)現沒有找到宰闰。結束

這里需要注意一下一些常用的額外參數的問題茬贵,整理如下:

  1. 如果啟動程序的時候設置了系統(tǒng)參數spring.profiles.active,那么這個參數會被設置到環(huán)境信息中(由于設置了系統(tǒng)參數移袍,在StandardEnvironment的鉤子方法customizePropertySources中被封裝成MapPropertySource并添加到Environment中)解藻。這樣PropertySourcesLoader加載的時候不會加上default這個默認profile,但是還是會讀取profile為null的配置信息咐容。spring.profiles.active支持多個profile舆逃,比如java -Dspring.profiles.active="dev,custom" -jar yourjar.jar
  2. 如果設置程序參數spring.config.location蚂维,那么查找目錄的時候會多出設置的目錄戳粒,也支持多個目錄的設置路狮。這些會在SpringApplication里的configureEnvironment方法中被封裝成SimpleCommandLinePropertySource并添加到Environment中。比如java -jar yourjar.jar --spring.config.location=classpath:/custom,file:./custom 1 2 3蔚约。有4個參數會被設置到SimpleCommandLinePropertySource中奄妨。解析文件的時候會多出2個目錄,分別是classpath:/custom和file:./custom
  3. 如果設置程序參數spring.config.name苹祟,那么查找的文件名就是這個參數值砸抛。原理跟spring.config.location一樣,都封裝到了SimpleCommandLinePropertySource中树枫。比如java -jar yourjar.jar --spring.config.name=myfile直焙。 這樣會去查找myfile文件,而不是默認的application文件
  4. 如果設置程序參數spring.profiles.active砂轻。注意這是程序參數奔誓,不是系統(tǒng)參數。比如java -jar yourjar.jar --spring.profiles.active=prod搔涝。會去解析prod這個profile(不論是系統(tǒng)參數還是程序參數厨喂,都會被封裝成多個PropertySource存在于環(huán)境信息中。最終獲取profile的時候會去環(huán)境信息中拿庄呈,且都可以拿到)
  5. 上面說的每個profile都是在不同文件里的蜕煌。不同profile也可以存在在一個文件里。因為有profile會去加載帶profile的文件的同時也會去加載不帶profile的文件诬留,并解析出這個文件中spring.profiles對應的值是profile的數據斜纪。比如profile為prod,會去查找application-prod.yml文件文兑,也會去查找application.yml文件傀广,其中application.yml文件只會查找spring.profiles為prod的數據

比如第6點中profile.yml的數據如下:

    spring:
        profiles: prod
    my.name: 1

    ---

    spring:
        profiles: dev
    my.name: 2

這里會解析出spring.profiles為prod的數據,也就是my.name為1的數據彩届。

優(yōu)先級的問題:由于環(huán)境信息Environment中保存的PropertySource是MutablePropertySources伪冰,那么會去配置值的時候就存在優(yōu)先級的問題。比如PropertySource1和PropertySource2都存在custom.name配置樟蠕,那么會從哪個PropertySource中獲取這個custom.name配置呢贮聂?它會遍歷內部的PropertySource列表,越在前面的PropertySource寨辩,越先獲认判浮;比如PropertySource1在PropertySource2前面靡狞,那么會先獲取PropertySource1的配置耻警。MutablePropertySources內部添加PropertySource的時候可以選擇元素的位置,可以addFirst,也可以addLast甘穿,也可以自定義位置腮恩。

總結:SpringApplication啟動的時候會構造環(huán)境信息Environment,如果是web環(huán)境温兼,創(chuàng)建StandardServletEnvironment秸滴,否則,創(chuàng)建StandardEnvironment募判。這兩種環(huán)境創(chuàng)建的時候都會在內部的propertySources屬性中加入一些PropertySource荡含。比如屬性屬性的配置信息封裝成MapPropertySource,系統(tǒng)環(huán)境配置信息封裝成SystemEnvironmentPropertySource等届垫。這些PropertySource集合存在在環(huán)境信息中释液,從環(huán)境信息中讀取配置的話會遍歷這些PropertySource并找到相對應的配置和值。Environment構造完成之后會讀取springboot相應的配置文件装处,從3個角度去查找:目錄均澳、文件名和profile。這3個角度有默認值符衔,可以進行覆蓋找前。springboot相關的配置文件讀取完成之后會被封裝成PropertySource并添加到環(huán)境信息中。

@ConfigurationProperties和@EnableConfigurationProperties注解的原理

SpringBoot內部規(guī)定了一套配置和配置屬性類映射規(guī)則判族,可以使用@ConfigurationProperties注解配合前綴屬性完成屬性類的讀忍墒ⅰ;再通過@EnableConfigurationProperties注解設置配置類就可以把這個配置類注入進來形帮。

比如ES的配置類ElasticsearchProperties和對應的@EnableConfigurationProperties修飾的類ElasticsearchAutoConfiguration:

// 使用前綴為spring.data.elasticsearch的配置
@ConfigurationProperties(prefix = "spring.data.elasticsearch")
public class ElasticsearchProperties {
    private String clusterName = "elasticsearch";
    private String clusterNodes;
    private Map<String, String> properties = new HashMap<String, String>();
    ...
}
@Configuration
@ConditionalOnClass({ Client.class, TransportClientFactoryBean.class,
        NodeClientFactoryBean.class })
// 使用@EnableConfigurationProperties注解讓ElasticsearchProperties配置生效
// 這樣ElasticsearchProperties就會自動注入到屬性中
@EnableConfigurationProperties(ElasticsearchProperties.class)
public class ElasticsearchAutoConfiguration implements DisposableBean {
    ...
    @Autowired
    private ElasticsearchProperties properties;
    ...
}

我們分析下這個過程的實現槽惫。

@EnableConfigurationProperties注解有個屬性value,是個Class數組辩撑,它會導入一個selector:EnableConfigurationPropertiesImportSelector界斜。這個selector的selectImport方法:

@Override
public String[] selectImports(AnnotationMetadata metadata) {
    // 獲取@EnableConfigurationProperties注解的屬性
    MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
            EnableConfigurationProperties.class.getName(), false);
    // 得到value屬性,是個Class數組
    Object[] type = attributes == null ? null
            : (Object[]) attributes.getFirst("value");
    if (type == null || type.length == 0) { // 如果value屬性不存在
        return new String[] {
                // 返回Registrar合冀,Registrar內部會注冊bean
                ConfigurationPropertiesBindingPostProcessorRegistrar.class
                        .getName() };
    }
    // 如果value屬性存在
    // 返回Registrar各薇,Registrar內部會注冊bean
    return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(),
            ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
}

ConfigurationPropertiesBeanRegistrar和ConfigurationPropertiesBindingPostProcessorRegistrar都實現了ImportBeanDefinitionRegistrar接口,會額外注冊bean君躺。

// ConfigurationPropertiesBeanRegistrar的registerBeanDefinitions方法
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
        BeanDefinitionRegistry registry) {
    // 獲取@EnableConfigurationProperties注解中的屬性值Class數組
    MultiValueMap<String, Object> attributes = metadata
            .getAllAnnotationAttributes(
                    EnableConfigurationProperties.class.getName(), false);
    List<Class<?>> types = collectClasses(attributes.get("value"));
    // 遍歷這些Class數組
    for (Class<?> type : types) {
        // 如果這個class被@ConfigurationProperties注解修飾
        // 獲取@ConfigurationProperties注解中的前綴屬性
        // 否則該前綴為空字符串
        String prefix = extractPrefix(type);
        // 構造bean的名字: 前綴-類全名
        // 比如ElasticsearchProperties對應的bean名字就是spring.data.elasticsearch-org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchProperties
        String name = (StringUtils.hasText(prefix) ? prefix + "-" + type.getName()
                : type.getName());
        if (!registry.containsBeanDefinition(name)) {
            // 這個bean沒被注冊的話進行注冊
            registerBeanDefinition(registry, type, name);
        }
    }
}

// ConfigurationPropertiesBindingPostProcessorRegistrar的registerBeanDefinitions方法
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
        BeanDefinitionRegistry registry) {
    // 先判斷Spring容器里是否有ConfigurationPropertiesBindingPostProcessor類型的bean
    // 由于條件里面會判斷是否已經存在這個ConfigurationPropertiesBindingPostProcessor類型的bean
    // 所以實際上條件里的代碼只會執(zhí)行一次
    if (!registry.containsBeanDefinition(BINDER_BEAN_NAME)) {
        BeanDefinitionBuilder meta = BeanDefinitionBuilder
                .genericBeanDefinition(ConfigurationBeanFactoryMetaData.class);
        BeanDefinitionBuilder bean = BeanDefinitionBuilder.genericBeanDefinition(
                ConfigurationPropertiesBindingPostProcessor.class);
        bean.addPropertyReference("beanMetaDataStore", METADATA_BEAN_NAME);
        registry.registerBeanDefinition(BINDER_BEAN_NAME, bean.getBeanDefinition());
        registry.registerBeanDefinition(METADATA_BEAN_NAME, meta.getBeanDefinition());
    }
}

ConfigurationPropertiesBindingPostProcessor在ConfigurationPropertiesBindingPostProcessorRegistrar中被注冊到Spring容器中峭判,它是一個BeanPostProcessor,它的postProcessBeforeInitialization方法如下:

// Spring容器中bean被實例化之前要做的事
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName)
        throws BeansException {
    // 先獲取bean對應的Class中的@ConfigurationProperties注解
    ConfigurationProperties annotation = AnnotationUtils
            .findAnnotation(bean.getClass(), ConfigurationProperties.class);
    // 如果@ConfigurationProperties注解棕叫,說明這是一個配置類林螃。比如ElasticsearchProperties
    if (annotation != null) {
        // 調用postProcessBeforeInitialization方法
        postProcessBeforeInitialization(bean, beanName, annotation);
    }
    // 同樣的方法使用beanName去查找
    annotation = this.beans.findFactoryAnnotation(beanName,
            ConfigurationProperties.class);
    if (annotation != null) {
        postProcessBeforeInitialization(bean, beanName, annotation);
    }
    return bean;
}

private void postProcessBeforeInitialization(Object bean, String beanName,
        ConfigurationProperties annotation) {
    Object target = bean;
    // 構造一個PropertiesConfigurationFactory
    PropertiesConfigurationFactory<Object> factory = new PropertiesConfigurationFactory<Object>(
            target);
    // 設置屬性源,這里的屬性源從環(huán)境信息Environment中得到
    factory.setPropertySources(this.propertySources);
    // 設置驗證器
    factory.setValidator(determineValidator(bean));
    // 設置ConversionService
    factory.setConversionService(this.conversionService == null
            ? getDefaultConversionService() : this.conversionService);
    if (annotation != null) {
        // 設置@ConfigurationProperties注解對應的屬性到PropertiesConfigurationFactory中
        // 比如是否忽略不合法的屬性ignoreInvalidFields俺泣、忽略未知的字段疗认、忽略嵌套屬性完残、驗證器驗證不合法后是否拋出異常
        factory.setIgnoreInvalidFields(annotation.ignoreInvalidFields());
        factory.setIgnoreUnknownFields(annotation.ignoreUnknownFields());
        factory.setExceptionIfInvalid(annotation.exceptionIfInvalid());
        factory.setIgnoreNestedProperties(annotation.ignoreNestedProperties());
        if (StringUtils.hasLength(annotation.prefix())) {
            // 設置前綴
            factory.setTargetName(annotation.prefix());
        }
    }
    try {
        // 綁定屬性到配置類中,比如ElasticsearchProperties
        // 會使用環(huán)境信息中的屬性源進行綁定
        // 這樣配置類就讀取到了配置文件中的配置
        factory.bindPropertiesToTarget();
    }
    catch (Exception ex) {
        String targetClass = ClassUtils.getShortName(target.getClass());
        throw new BeanCreationException(beanName, "Could not bind properties to "
                + targetClass + " (" + getAnnotationDetails(annotation) + ")", ex);
    }
}

總結:SpringBoot內部規(guī)定了一套配置和配置屬性類映射規(guī)則横漏,可以使用@ConfigurationProperties注解配合前綴屬性完成屬性類的讀冉魃琛;再通過@EnableConfigurationProperties注解設置配置類就可以把這個配置類注入進來绊茧。由于這個配置類是被注入進來的铝宵,所以它肯定在Spring容器中存在打掘;這是因為在ConfigurationPropertiesBeanRegistrar內部會注冊配置類到Spring容器中华畏,這個配置類的實例化過程在ConfigurationPropertiesBindingPostProcessor這個BeanPostProcessor完成,它會在實例化bean之前會判斷bean是否被@ConfigurationProperties注解修飾尊蚁,如果有亡笑,使用PropertiesConfigurationFactory從環(huán)境信息Environment中進行值的綁定。這個ConfigurationPropertiesBeanRegistrar是在使用@EnableConfigurationProperties注解的時候被創(chuàng)建的(通過EnableConfigurationPropertiesImportSelector)横朋。配置類內部屬性的綁定成功與否是通過環(huán)境信息Environment中的屬性源PropertySource決定的仑乌。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市琴锭,隨后出現的幾起案子晰甚,更是在濱河造成了極大的恐慌,老刑警劉巖决帖,帶你破解...
    沈念sama閱讀 218,755評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件厕九,死亡現場離奇詭異,居然都是意外死亡地回,警方通過查閱死者的電腦和手機扁远,發(fā)現死者居然都...
    沈念sama閱讀 93,305評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刻像,“玉大人畅买,你說我怎么就攤上這事∠杆” “怎么了谷羞?”我有些...
    開封第一講書人閱讀 165,138評論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長溜徙。 經常有香客問我洒宝,道長,這世上最難降的妖魔是什么萌京? 我笑而不...
    開封第一講書人閱讀 58,791評論 1 295
  • 正文 為了忘掉前任雁歌,我火速辦了婚禮,結果婚禮上知残,老公的妹妹穿的比我還像新娘靠瞎。我一直安慰自己比庄,他們只是感情好,可當我...
    茶點故事閱讀 67,794評論 6 392
  • 文/花漫 我一把揭開白布乏盐。 她就那樣靜靜地躺著佳窑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪父能。 梳的紋絲不亂的頭發(fā)上神凑,一...
    開封第一講書人閱讀 51,631評論 1 305
  • 那天,我揣著相機與錄音何吝,去河邊找鬼溉委。 笑死,一個胖子當著我的面吹牛爱榕,可吹牛的內容都是我干的瓣喊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,362評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼黔酥,長吁一口氣:“原來是場噩夢啊……” “哼藻三!你這毒婦竟也來了?” 一聲冷哼從身側響起跪者,我...
    開封第一講書人閱讀 39,264評論 0 276
  • 序言:老撾萬榮一對情侶失蹤棵帽,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后渣玲,有當地人在樹林里發(fā)現了一具尸體逗概,經...
    沈念sama閱讀 45,724評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,900評論 3 336
  • 正文 我和宋清朗相戀三年柜蜈,在試婚紗的時候發(fā)現自己被綠了仗谆。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,040評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡淑履,死狀恐怖隶垮,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情秘噪,我是刑警寧澤狸吞,帶...
    沈念sama閱讀 35,742評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站指煎,受9級特大地震影響蹋偏,放射性物質發(fā)生泄漏。R本人自食惡果不足惜至壤,卻給世界環(huán)境...
    茶點故事閱讀 41,364評論 3 330
  • 文/蒙蒙 一威始、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧像街,春花似錦黎棠、人聲如沸晋渺。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽木西。三九已至,卻和暖如春随静,著一層夾襖步出監(jiān)牢的瞬間八千,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評論 1 270
  • 我被黑心中介騙來泰國打工燎猛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留恋捆,地道東北人。 一個月前我還...
    沈念sama閱讀 48,247評論 3 371
  • 正文 我出身青樓扛门,卻偏偏與公主長得像鸠信,于是被迫代替她去往敵國和親纵寝。 傳聞我的和親對象是個殘疾皇子论寨,可洞房花燭夜當晚...
    茶點故事閱讀 44,979評論 2 355

推薦閱讀更多精彩內容