springboot SPI 擴(kuò)展機(jī)制

springboot的擴(kuò)展解耦,仿照java的SPI機(jī)制飘蚯,使用SpringFactoriesLoader加載spring.factories文件實(shí)現(xiàn)兄朋。

java SPI(Service Provider Inteface)


java中為廠商或插件設(shè)計(jì)的擴(kuò)展機(jī)制,為什么引入脂倦?

  • 系統(tǒng)中抽象的各個(gè)模塊畅厢,比如日志模塊冯痢,xml解析模塊、jdbc模塊等框杜,每個(gè)模塊有多種實(shí)現(xiàn)方案浦楣。
  • 面向?qū)ο蟪绦蛟O(shè)計(jì)中,一般推薦模塊間基于接口編程咪辱,模塊間不對(duì)實(shí)現(xiàn)類進(jìn)行硬編碼振劳。一旦代碼中涉及具體的實(shí)現(xiàn)類,就違反了可拔插的原則油狂。
  • 如果需要替換一種實(shí)現(xiàn)历恐,就需要修改代碼寸癌。為了實(shí)現(xiàn)在模塊裝配時(shí),能不在程序里動(dòng)態(tài)指明弱贼,就需要一種服務(wù)發(fā)現(xiàn)機(jī)制蒸苇。

解決方案

  • java SPI, 為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)的機(jī)制吮旅,類似IOC思想溪烤,將裝配的控制權(quán)移到程序之外,在模塊化設(shè)計(jì)中該機(jī)制尤其重要
    也即 Java提供的SPI接口和調(diào)用方在java的核心類庫(kù)庇勃,接口的具體實(shí)現(xiàn)類由廠商或插件設(shè)計(jì)開發(fā)檬嘀,

java虛擬機(jī)中采用雙親委派模型進(jìn)行類的加載,而java SPI實(shí)現(xiàn)類的加載责嚷,不適用雙親委派模型鸳兽,因而有了破壞雙親委派模型的說(shuō)法。為什么說(shuō) Java SPI 的設(shè)計(jì)違反雙親委派原則

雖然用了“破壞”這個(gè)詞再层,并不帶有貶義贸铜,只要有足夠意義和理由,突破已有的原則就可認(rèn)為是創(chuàng)新聂受。秉持這個(gè)觀點(diǎn),再來(lái)看spring 私有框架的擴(kuò)展類加載過(guò)程烤镐,并不符合傳統(tǒng)的雙親委派模型的類加載蛋济,仍值得學(xué)習(xí),弄懂其實(shí)現(xiàn)炮叶,就可掌握在spring基礎(chǔ)上擴(kuò)展的各類組件及工具集碗旅。諸如spring boot、spring cloud 等镜悉。

spring SPI擴(kuò)展機(jī)制


類似Java SPI擴(kuò)展加載機(jī)制祟辟。在META-INF/spring.factories文件中配置接口的實(shí)現(xiàn)類名稱,程序中讀取這些配置文件并實(shí)例化侣肄。這種自定義的SPI機(jī)制是Spring Boot Starter實(shí)現(xiàn)的基礎(chǔ)旧困。

在開始介紹SpringFactoriesLoader加載spring.factories文件前,先了解java類加載器加載資源的過(guò)程

java的類加載器除加載 class 外稼锅,還有一個(gè)重要功能就是加載資源吼具,從 jar 包中讀取任何資源文件,如ClassLoader.getResources(Stringname) 方法讀取 jar 包中的資源文件矩距,代碼如下:

    public URL getResource(String name) {
        URL url;
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            url = getBootstrapResource(name);
        }
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }

加載資源的過(guò)程拗盒,與雙親委派模型類加載的過(guò)程一樣,首先判斷父類是否為空锥债,不為空則將任務(wù)委派給父類加載器執(zhí)行資源加載陡蝇,直到啟動(dòng)類加載器BootstrapClassLoader痊臭。最后才輪到自己查找,而不同的類加載器負(fù)責(zé)掃描不同路徑下的 jar 包登夫,就如同加載 class 一樣趣兄,最后會(huì)掃描所有的 jar 包,找到符合條件的資源文件悼嫉。

類加載器的 ClassLoader.findResources(name) 方法會(huì)遍歷其負(fù)責(zé)加載的所有 jar 包艇潭,找到 jar 包中名稱為 name 的資源文件,這里的資源可以是任何文件戏蔑,甚至是 .class 文件蹋凝,比如下面的示例,用于查找 ConcurrentHashMap.class 文件:

    public static void main(String[] args) throws IOException {
        String name = "java/util/concurrent/ConcurrentHashMap.class";
        Enumeration<URL> urls = Thread.currentThread()
                                .getContextClassLoader().getResources(name);
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            System.out.println(url.toString());
        }
    }

運(yùn)行的結(jié)果

jar:file:/C:/Program%20Files/Java/jdk1.8.0_25/jre/lib/rt.jar!/java/util/concurrent/ConcurrentHashMap.class

有了ClassLoder加載資源文件的知識(shí)总棵,接下來(lái)了解SpringFactoriesLoader加載spring.factories文件過(guò)程

spring-core包中定義了SpringFactoriesLoader類鳍寂,該類定義兩個(gè)對(duì)外方法

  • loadFactories 根據(jù)接口類獲取其實(shí)現(xiàn)類的實(shí)例,返回對(duì)象列表
  • loadFactoryNames 根據(jù)接口獲取其接口類的名稱情龄,返回類名列表

loadFactoryNames()源碼實(shí)現(xiàn):

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

//spring.factories文件的格式為:key=value1,value2,value3
//從所有jar文件中找到MET-INF/spring.factories文件
//然后從文件中解析初key=factoryClass類名稱的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
    String factoryClassName = factoryClass.getName();
    try {
//取得資源文件的URL
        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        List<String> result = new ArrayList<String>();
//遍歷所有的URL
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
//根據(jù)資源文件的url解析properties
            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
            String factoryClassNames = properties.getProperty(factoryClassName);

    //組裝數(shù)據(jù)并返回
      result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
        }
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);

    }

}

從 ClassPath 下的每個(gè) Jar 包中搜尋所有 META-INF/spring.factories 配置文件迄汛,然后解析 properties 文件,找到指定名稱的配置后返回骤视。
注意鞍爱,不僅在ClassPath 路徑下查找,也會(huì)掃描所有路徑下的 Jar 包专酗,只不過(guò)spring.factories只存在于 Classpath 下的 jar 包中睹逃。spring.factories 示例:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
...

執(zhí)行 loadFactoryNames(EnableAutoConfiguration.class,classLoader) ,得到對(duì)應(yīng)的一組 @Configuration 類祷肯,通過(guò)反射實(shí)例化這些類注入到 IOC 容器中沉填,從而容器中擁有了一系列標(biāo)注 @Configuration的JavaConfig 形式的配置類。

SpringFactoriesLoader本質(zhì)上屬于 Spring 框架私有的一種擴(kuò)展方案佑笋,類似于 SPI翼闹,即不采用雙親委派模型加載類,Spring Boot 在 Spring 基礎(chǔ)上擴(kuò)展的很多核心功能都是基于此機(jī)制實(shí)現(xiàn)

spring.factories文件加載過(guò)程詳解


1蒋纬、啟動(dòng)類入口
2猎荠、springboot使用啟動(dòng)類注解@SpringBootApplication
3、進(jìn)入@EnableAutoConfiguration颠锉,通過(guò)@Import加載EnableAutoConfigurationImportSelector類法牲, @import作用是:spring IOC容器中沒有注入EnableAutoConfigurationImportSelector類,但springboot啟動(dòng)需要用到琼掠,因此通過(guò)@Import注解將該類注入到spring容器

@SpringBootApplication
public class Application {
public static void main(String[] args) {
     SpringApplication.run(Application.class);
}


@SpringBootConfiguration
@EnableAutoConfiguration
public @interface SpringBootApplication {}


@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}

springboot通過(guò)@SpringBootApplication注解拒垃,在spring的基礎(chǔ)上進(jìn)行功能擴(kuò)展。引入其他業(yè)務(wù)組建的平臺(tái)邏輯(擴(kuò)展操作)瓷蛙,在@Import(EnableAutoConfigurationImportSelector.class)中實(shí)現(xiàn)悼瓮,spring cloud config/ spring cloud eureka等組件均通過(guò)該方式構(gòu)建

EnableAutoConfigurationImportSelector為ImportSelector實(shí)現(xiàn)類戈毒,所有ImportSelector接口實(shí)現(xiàn)類,會(huì)在spring容器啟動(dòng)時(shí)被ConfigurationClassParser.processImports()實(shí)例化横堡,并執(zhí)行selectImports方法埋市。 springboot啟動(dòng),是先進(jìn)行注解@SpringBootApplication的掃描命贴,還是先運(yùn)行SpringApplication.run(Application.class)道宅,由上述分析可知,注解掃描優(yōu)先

4胸蛛、EnableAutoConfigurationImportSelector類

@Deprecated
public class EnableAutoConfigurationImportSelector
        extends AutoConfigurationImportSelector {

@Import和xml配置的 <import />標(biāo)簽作用一樣污茵,允許通過(guò)它引入 @Configuration 注解的類 (java config), 引入ImportSelector接口(要通過(guò)它去判定要引入哪些@Configuration) 和 ImportBeanDefinitionRegistrar 接口的實(shí)現(xiàn)葬项, 也包括 @Component注解的普通類泞当。但是如果要引入另一個(gè)xml 文件形式配置的 bean, 則需要通過(guò) @ImportResource 注解。

EnableAutoConfigurationImportSelector繼承AutoConfigurationImportSelector (繼承接口ImportSelector)類民珍,并覆蓋其isEnabled方法襟士,后續(xù)實(shí)例化EnableAutoConfigurationImportSelector時(shí),調(diào)用的isEnabled方法嚷量,取自覆蓋后的方法陋桂。

ImportSelector接口

@Import 實(shí)現(xiàn),通常要借助 ImportSelector 接口的實(shí)現(xiàn)類決定引入哪些 @Configuration津肛。 如果ImportSelector實(shí)現(xiàn)類章喉,實(shí)現(xiàn)了以下四個(gè)Aware 接口的一個(gè)或多個(gè)(EnvironmentAware、BeanFactoryAware身坐、BeanClassLoaderAware、ResourceLoaderAware)落包, 在bean生命周期的初始化階段部蛇, 會(huì)先回調(diào)aware接口方法的實(shí)現(xiàn),織入系統(tǒng)變量咐蝇,使得實(shí)例化bean擁有操作系統(tǒng)變量的能力涯鲁。

/**
 * Interface to be implemented by types that determine which @{@link Configuration}
 * class(es) should be imported based on a given selection criteria, usually one or more
 * annotation attributes.
 *
 * An {@link ImportSelector} may implement any of the following
 * {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective
 * methods will be called prior to {@link #selectImports}:
 * {@link org.springframework.context.EnvironmentAware EnvironmentAware}
 * {@link org.springframework.beans.factory.BeanFactoryAware BeanFactoryAware}
 * {@link org.springframework.beans.factory.BeanClassLoaderAware BeanClassLoaderAware}
 * {@link org.springframework.context.ResourceLoaderAware ResourceLoaderAware}
 * */
public interface ImportSelector {

    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

5、AutoConfigurationImportSelector.selectImports()方法

@Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        try {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                    .loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = getAttributes(annotationMetadata);
            List<String> configurations = getCandidateConfigurations(annotationMetadata,
                    attributes);
            configurations = removeDuplicates(configurations);
            configurations = sort(configurations, autoConfigurationMetadata);
            Set<String> exclusions = getExclusions(annotationMetadata, attributes);
            checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = filter(configurations, autoConfigurationMetadata);
            fireAutoConfigurationImportEvents(configurations, exclusions);
            return configurations.toArray(new String[configurations.size()]);
        }
        catch (IOException ex) {
            throw new IllegalStateException(ex);
        }
    }

為了選出想要加載的import類有序,如何獲取呢抹腿?其實(shí)是通過(guò)SpringFactoriesLoader去加載對(duì)應(yīng)的spring.factories

下面展示如何和此類建立關(guān)系。
進(jìn)入selectImports() -> getCandidateConfigurations()

    /**
     * Return the auto-configuration class names that should be considered. By default
     * this method will load candidates using {@link SpringFactoriesLoader} with
     * {@link #getSpringFactoriesLoaderFactoryClass()}.
     * @param metadata the source metadata
     * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
     * attributes}
     * @return a list of candidate configurations
     */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }


    /**
     * Return the class used by {@link SpringFactoriesLoader} to load configuration
     * 返回SpringFactoriesLoader類加載配置需要的接口類
     * candidates.
     * @return the factory class
     */
    protected Class<?> getSpringFactoriesLoaderFactoryClass() {
        return EnableAutoConfiguration.class;
    }

注釋中的描述旭寿,使用SpringFactoriesLoader類警绩、AutoConfigurationImportSelector.getSpringFactoriesLoaderFactoryClass()組合,加載指定資源盅称。
SpringFactoriesLoader.loadFactoryNames所需的參數(shù)肩祥,其中一個(gè)參數(shù)通過(guò)調(diào)用getSpringFactoriesLoaderFactoryClass()后室,獲取返回值EnableAutoConfiguration.class ,

接下來(lái)SpringFactoriesLoader根據(jù)這個(gè)interface混狠,查找所有spring.factories中EnableAutoConfiguration.class對(duì)應(yīng)的values岸霹,并返回。

SpringFactoriesLoader如何加載


loadFactoryNames方法

/**
     * Load the fully qualified class names of factory implementations of the
     * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
     * class loader.
     * @param factoryClass the interface or abstract class representing the factory
     * @param classLoader the ClassLoader to use for loading resources; can be
     * {@code null} to use the default
     * @see #loadFactories
     * @throws IllegalArgumentException if an error occurs while loading factory names
     */
//factoryClass傳入的參數(shù)值為EnableAutoConfiguration.class
    public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName(); //factoryClassName的取值為“EnableAutoConfiguration"
        try {
//FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"
//urls為查找到的spring.factories文件列表
            Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                    ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
            List<String> result = new ArrayList<String>();
            while (urls.hasMoreElements()) {
//遍歷spring.factories資源文件
                URL url = urls.nextElement();
//解析文件中的屬性值将饺,存入Properties 
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
//根據(jù)鍵EnableAutoConfiguration找到配置文件中對(duì)應(yīng)的屬性值
                String factoryClassNames = properties.getProperty(factoryClassName);
                result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); //將取到的values按逗號(hào)分隔贡避,并轉(zhuǎn)換成list
            }
            return result; //返回取值list
        }
        catch (IOException ex) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
                    "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
        }
    }

返回值為list,取值從org.springframework.boot.autoconfigure.EnableAutoConfiguration的value={‘org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration’予弧,‘org.springframework.boot.autoconfigure.aop.AopAutoConfiguration’刮吧,‘org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration’,桌肴,皇筛,,坠七,水醋,}

Springboot 對(duì)@Import注解的處理過(guò)程
入口
AbstractApplicationContext.refresh() 
-> AbstractApplicationContext.invokeBeanFactoryPostProcessors(beanFactory)
->PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) 
->ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) 
-> ConfigurationClassPostProcessor.processConfigBeanDefinitions(BeanDefinitionRegistry registry)
  • springboot初始化的普通context(非web) 是AnnotationConfigApplicationContext(spring注解容器)
  • 在初始化的時(shí)候會(huì)初始化兩個(gè)工具類, AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 分別用來(lái)從 annotation driven 的配置和xml的配置中讀取beanDefinition并向context注冊(cè),
  • 那么在初始化 AnnotatedBeanDefinitionReader 的時(shí)候彪置, 會(huì)向BeanFactory注冊(cè)一個(gè)ConfigurationClassPostProcessor 用來(lái)處理所有的基于annotation的bean, 這個(gè)ConfigurationClassPostProcessor 是 BeanFactoryPostProcessor 的一個(gè)實(shí)現(xiàn)拄踪,springboot會(huì)保證在 invokeBeanFactoryPostProcessors(beanFactory) 方法中調(diào)用注冊(cè)到它上邊的所有的BeanFactoryPostProcessor
  • 因此,在spring容器啟動(dòng)時(shí)拳魁,會(huì)調(diào)用ConfigurationClassPostProcessor .postProcessBeanDefinitionRegistry()方法惶桐。
ConfigurationClassParser

在ConfigurationClassPostProcessor .postProcessBeanDefinitionRegistry()方法中實(shí)例化ConfigurationClassParser調(diào)用

// Parse each @Configuration class
        ConfigurationClassParser parser = new ConfigurationClassParser(
                this.metadataReaderFactory, this.problemReporter, this.environment,
                this.resourceLoader, this.componentScanBeanNameGenerator, registry);

在 ConfigurationClassParser -> processConfigurationClass() -> doProcessConfigurationClass() 方法中找到( 分別按次序處理@PropertySource, @ComponentScan, @Import, @ImportResource, 在處理這些注解時(shí)潘懊,通過(guò)遞歸保證所有的都被處理)
取重點(diǎn)代碼段

// Process any @Import annotations
        processImports(configClass, sourceClass, getImports(sourceClass), true);

processImports流程如下:

  • 首先姚糊,如果import 是 ImportSelector.class 接口的實(shí)現(xiàn), 則初始化被Import的類授舟, 再調(diào)用它的selectImports方法獲得引入的configuration救恨, 遞歸處理
  • 其次,如果import是 ImportBeanDefinitionRegistrar 接口的實(shí)現(xiàn)释树, 則初始化后將對(duì)當(dāng)前對(duì)象的處理委托給這個(gè)ImportBeanDefinitionRegistrar (不是特別明白肠槽, 只是我的猜測(cè))
  • 最后, 將import引入的類作為一個(gè)正常的類來(lái)處理 ( 調(diào)用最外層的doProcessConfigurationClass())

綜上奢啥, 如果引入正常的component秸仙, 會(huì)作為@Component 或 @Configuration處理, 在BeanFactory中通過(guò)getBean()獲取桩盲, 但如果是 ImportSelector 或ImportBeanDefinitionRegistrar 接口的實(shí)現(xiàn)寂纪, spring不會(huì)將它們注冊(cè)到beanFactory中,而只是調(diào)用它們的方法正驻。

回顧本文之前所講springboot啟動(dòng)弊攘,@SpringBootApplication注解通過(guò)@Import注入到spring容器的EnableAutoConfigurationImportSelector類抢腐,其繼承的父類方法AutoConfigurationImportSelector.selectImports()在此處被調(diào)用。

processImports實(shí)現(xiàn)代碼塊如下

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {
                襟交,迈倍,,捣域,
                for (SourceClass candidate : importCandidates) {
                    if (candidate.isAssignable(ImportSelector.class)) {
                        // Candidate class is an ImportSelector -> delegate to it to determine imports
                        Class<?> candidateClass = candidate.loadClass();
                        ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                selector, this.environment, this.resourceLoader, this.registry);
                        if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
                            this.deferredImportSelectors.add(
                                    new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
                        }
                        else {
                            String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
                            Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
                            processImports(configClass, currentSourceClass, importSourceClasses, false);
                        }
                    }
                    else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
                        // Candidate class is an ImportBeanDefinitionRegistrar ->
                        // delegate to it to register additional bean definitions
                        Class<?> candidateClass = candidate.loadClass();
                        ImportBeanDefinitionRegistrar registrar =
                                BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
                        ParserStrategyUtils.invokeAwareMethods(
                                registrar, this.environment, this.resourceLoader, this.registry);
                        configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
                    }
                    else {
                        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
                        processConfigurationClass(candidate.asConfigClass(configClass));
                    }
                }
            啼染,,焕梅,迹鹅,
        }
    }

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市贞言,隨后出現(xiàn)的幾起案子斜棚,更是在濱河造成了極大的恐慌,老刑警劉巖该窗,帶你破解...
    沈念sama閱讀 218,640評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件弟蚀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡酗失,警方通過(guò)查閱死者的電腦和手機(jī)义钉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,254評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)规肴,“玉大人捶闸,你說(shuō)我怎么就攤上這事⊥先校” “怎么了删壮?”我有些...
    開封第一講書人閱讀 165,011評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)兑牡。 經(jīng)常有香客問(wèn)我醉锅,道長(zhǎng),這世上最難降的妖魔是什么发绢? 我笑而不...
    開封第一講書人閱讀 58,755評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮垄琐,結(jié)果婚禮上边酒,老公的妹妹穿的比我還像新娘。我一直安慰自己狸窘,他們只是感情好墩朦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,774評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著翻擒,像睡著了一般氓涣。 火紅的嫁衣襯著肌膚如雪牛哺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,610評(píng)論 1 305
  • 那天劳吠,我揣著相機(jī)與錄音引润,去河邊找鬼。 笑死痒玩,一個(gè)胖子當(dāng)著我的面吹牛淳附,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蠢古,決...
    沈念sama閱讀 40,352評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼奴曙,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了草讶?” 一聲冷哼從身側(cè)響起洽糟,我...
    開封第一講書人閱讀 39,257評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堕战,沒想到半個(gè)月后坤溃,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,717評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡践啄,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,894評(píng)論 3 336
  • 正文 我和宋清朗相戀三年浇雹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屿讽。...
    茶點(diǎn)故事閱讀 40,021評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡昭灵,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出伐谈,到底是詐尸還是另有隱情烂完,我是刑警寧澤,帶...
    沈念sama閱讀 35,735評(píng)論 5 346
  • 正文 年R本政府宣布诵棵,位于F島的核電站抠蚣,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏履澳。R本人自食惡果不足惜嘶窄,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,354評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望距贷。 院中可真熱鬧柄冲,春花似錦、人聲如沸忠蝗。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,936評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至戒祠,卻和暖如春骇两,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背姜盈。 一陣腳步聲響...
    開封第一講書人閱讀 33,054評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工低千, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贩据。 一個(gè)月前我還...
    沈念sama閱讀 48,224評(píng)論 3 371
  • 正文 我出身青樓栋操,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親饱亮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子矾芙,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,974評(píng)論 2 355