Service Provider Interface詳解 (SPI)

1.介紹

熟悉JDBC的同學(xué)都知道,在jdbc4.0之前茫负,在使用DriverManager獲取DB連接之前蕉鸳,我們總是需要顯示的實(shí)例化DB驅(qū)動(dòng)。比如忍法,對mysql潮尝,典型的代碼如下:

Connection conn = null;
Statement stmt = null;
try{
    // 注冊 JDBC driver
    Class.forName("com.mysql.jdbc.Driver");
    
    // 打開連接
    conn = DriverManagger.getConnection(DB_URL,USER,PASSWD);
    
    // 執(zhí)行一條sql
    stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery(sql);
    
    // 數(shù)據(jù)解包
    while(ts.next()){
        // 根據(jù)列名獲取列值
        // ...
    } catch(SQLException se) {
        // ...
    } final {
        try {
            if (stmt!=null) stmt.close();
        } catch(Exception e) {/*ignored*/}
        try {
            if (conn!=null) conn.close();
        } catch(Exception e) {/*ignored*/}
    }
}

JDBC的開始,總是需要通過Class.forName顯式實(shí)例化驅(qū)動(dòng)饿序,否則將找不到對應(yīng)DB的驅(qū)動(dòng)勉失。但是JDBC4.0開始,這個(gè)顯式的初始化不再是必選項(xiàng)了原探,它存在的意義只是為了向上兼容乱凿。那么JDBC4.0之后,我們的應(yīng)用是如何找到對應(yīng)的驅(qū)動(dòng)呢咽弦?

答案就是SPI(Service Provider Interface)徒蟆。Java在語言層面為我們提供了一種方便地創(chuàng)建可擴(kuò)展應(yīng)用的途徑。SPI提供了一種JVM級(jí)別的服務(wù)發(fā)現(xiàn)機(jī)制型型,我們只需要按照SPI的要求段审,在jar包中進(jìn)行適當(dāng)?shù)呐渲茫琷vm就會(huì)在運(yùn)行時(shí)通過懶加載闹蒜,幫我們找到所需的服務(wù)并加載寺枉。如果我們一直不使用某個(gè)服務(wù)易稠,那么它不會(huì)被加載,一定程度上避免了資源的浪費(fèi)好乐。

2.一個(gè)簡單的例子

我們通過一個(gè)簡單的例子看看如何最小化構(gòu)建一個(gè)基于SPI的服務(wù)流酬。

2.1 創(chuàng)建一個(gè)默認(rèn)的maven項(xiàng)目

$ mvn archetype:generate -DgroupId=cn.jinlu.spi.demo -DartifactId=simplespi -Dversion=0.1-SNAPSHOT -DpackageName=cn.jinlu.spi.demo -DarchetypeArtifactId=maven-archetype-quickstart
...
[INFO] Using property: groupId = cn.jinlu.spi.demo
[INFO] Using property: artifactId = simplespi
[INFO] Using property: version = 0.1-SNAPSHOT
[INFO] Using property: package = cn.jinlu.spi.demo
Confirm properties configuration:
groupId: cn.jinlu.spi.demo
artifactId: simplespi
version: 0.1-SNAPSHOT
package: cn.jinlu.spi.demo
 Y: :[回車]

2.2 添加一個(gè)interface或abstract class

Java SPI并沒有強(qiáng)制必須使用interface或abstract class,完全可以將class注冊為SPI注冊服務(wù)甘畅,但是作為可擴(kuò)展服務(wù)埂蕊,使用interface或abstract class是一個(gè)好習(xí)慣。

在包 “cn.jinlu.spi.demo”中定義一個(gè)接口Animal:

package cn.jinlu.spi.demo;

public interface Animal {
    void eat();
    void sleep();
}

2.3 提供實(shí)現(xiàn)類

package cn.jinlu.spi.demo.impl;

import cn.jinlu.spi.demo.Animal;

public class Elephant implements Animal {
    @Override
    public void eat() {
        System.out.println("Elephant is eating");
    }

    @Override
    public void sleep() {
        System.out.println("Elephant is sleeping");
    }
}

2.4 服務(wù)注冊

在main目錄下創(chuàng)建目錄 "resources/META-INF/services"

mkdir -p resources/META-INF/services

再在該目錄下創(chuàng)建以接口Animal全限定名為名的配置文件疏唾,文件內(nèi)容為該接口的實(shí)現(xiàn)類的全限定名蓄氧,即

echo "cn.jinlu.spi.demo.impl.Elephant" > resources/META-INF/services/cn.jinlu.spi.demo.Animal

完成此步驟后,在當(dāng)前maven項(xiàng)目的 src/main/resources/META-INF/services下有這么一個(gè)配置文件:"cn.jinlu.spi.demo.Animal"槐脏,并且它的內(nèi)容為"cn.jinlu.spi.demo.impl.Elephant"喉童。

注意本步驟的要點(diǎn):

  1. 必須放在JAR包或項(xiàng)目的指定路徑,即 META-INF/services 下
  2. 必須以服務(wù)的全限定名命名配置文件顿天,比如本例中堂氯,配置文件必須命名為 cn.jinlu.spi.demo.Animal,java會(huì)根據(jù)此名進(jìn)行服務(wù)查找
  3. 內(nèi)容必須是一個(gè)實(shí)現(xiàn)類的全限定名牌废,如果要注冊多個(gè)實(shí)現(xiàn)類咽白,按行分割。注釋以#開頭鸟缕。

2.5 增加單元測試:

注意晶框,如果找不到@Test,可能是junit版本太低懂从,在pom.xml中將其改為 4.0 或更高版本(maven-archetype-quickstart模板默認(rèn)的JUNIT目前是3.8.1版本)授段。

package cn.jinlu.spi.demo;

import org.junit.Test;

import java.util.ServiceLoader;

public class AnimalTest {
    @Test
    public void animalTest() {
        ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
        for(Animal animal: animals) {
            animal.eat();
            animal.sleep();
        }
    }
}

2.6 執(zhí)行結(jié)果:

Elephant is eating
Elephant is sleeping

可見,雖然我們沒有顯式使用Animal的實(shí)現(xiàn)類Elephant番甩,但是java幫我們自動(dòng)加載了改實(shí)現(xiàn)類侵贵。

3.源碼分析

接下來從代碼層面看看SPI都為我們做了什么。首先看看java.util.ServiceLoader的實(shí)現(xiàn)缘薛。在2.5節(jié)中窍育,我們看到ServiceLoader使用非常簡單,只需要調(diào)用一個(gè)靜態(tài)方法load并以要加載的服務(wù)的父類(通常是一個(gè)interface或abstract class)作為參數(shù)掩宜,jvm就會(huì)幫我們構(gòu)建好當(dāng)前進(jìn)程中所有注冊到 META-INF/services/[service full qualified class name] 的服務(wù)蔫骂。

3.1 創(chuàng)建ServiceLoader實(shí)例

下面是構(gòu)造ServiceLoader實(shí)例的相關(guān)代碼。ServiceLoader必須通過靜態(tài)方法load(Class<?> service)的方式加載服務(wù)牺汤,默認(rèn)會(huì)使用當(dāng)前線程的上下文class loader辽旋。構(gòu)造完ServiceLoader后,ServiceLoader實(shí)例并不會(huì)立刻掃描當(dāng)前進(jìn)程中的服務(wù)實(shí)例,而是創(chuàng)建一個(gè)LazyIterator懶加載迭代器补胚,在實(shí)際使用時(shí)再掃描所有jar包找到對應(yīng)的服務(wù)码耐。懶加載迭代器被保存在一個(gè)內(nèi)部成員lookupIterator中。

public final class ServiceLoader<S> implements Iterable<S>
{
    ...
    /**
     * 重新load指定serivice的實(shí)現(xiàn)溶其。通過LazyIterator實(shí)現(xiàn)懶加載骚腥。
     */
    public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }
    /**
     * ServiceLoader構(gòu)造函數(shù),私有類型瓶逃,必須通過ServiceLoader.load(Class<?>)靜態(tài)方法來創(chuàng)建ServiceLoader實(shí)例
     */
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    /**
     * 構(gòu)建ServiceLoader實(shí)例
     */
    public static <S> ServiceLoader<S> load(Class<S> service,
            ClassLoader loader)
    {
        return new ServiceLoader<>(service, loader);
    }
    /**
     * 通過service的class創(chuàng)建ServiceLoader實(shí)例束铭,默認(rèn)使用上下文classloader
     */
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
    ...
}

3.2 服務(wù)加載和遍歷

  • 上一節(jié)(3.1)的代碼中,我們可以看到厢绝,在調(diào)用了ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class)之后契沫,ServiceLoader會(huì)返回一個(gè)Animal.class類型的迭代器,但此時(shí)在ServiceLoader內(nèi)部只是創(chuàng)建了一個(gè)LazyIterator昔汉,而不會(huì)真正通過classloader在classpath中尋找相關(guān)的服務(wù)實(shí)現(xiàn)懈万。
  • 相反,ServiceLoader通過實(shí)現(xiàn)Iterable<?>接口(public final class ServiceLoader<S> implements Iterable<S>)靶病,將對服務(wù)實(shí)現(xiàn)的尋址延后倒了對animals的遍歷時(shí)執(zhí)行会通。它在ServiceLoader內(nèi)通過LazyIterator實(shí)現(xiàn)。
public final class ServiceLoader<S> implements Iterable<S>
{
    ...
    // 緩存的service provider娄周,按照初始化順序排列涕侈。
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 當(dāng)前的LazyIterator迭代器指針,服務(wù)懶加載迭代器
    private LazyIterator lookupIterator;

    ...
    // 創(chuàng)建ServiceLoader迭代器昆咽,隱藏了LazyIterator的實(shí)現(xiàn)細(xì)節(jié)
    public Iterator<S> iterator() {
        return new Iterator<S>() {

            // 創(chuàng)建Iterator迭代器時(shí)的ServiceLoader.providers快照驾凶,
            // 因此在首次迭代時(shí),iterator總是會(huì)通過LazyIterator進(jìn)行懶加載
            Iterator<Map.Entry<String,S>> knownProviders
                = providers.entrySet().iterator();

            public boolean hasNext() {
                // 如果已經(jīng)掃描過掷酗,則對providers進(jìn)行迭代;
                if (knownProviders.hasNext())
                    return true;
                // 如果沒有掃描過窟哺,則通過lookupIterator進(jìn)行掃描和懶加載
                return lookupIterator.hasNext();
            }

            public S next() {
                // 如果已經(jīng)掃描過泻轰,則對providers進(jìn)行迭代;
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 如果沒有掃描過且轨,則通過lookupIterator進(jìn)行掃描和懶加載
                return lookupIterator.next();
            }

            public void remove() {
                throw new UnsupportedOperationException();
            }

        };
    }
    ...
}

ServiceLoader的迭代器很簡單:

  1. 未進(jìn)行迭代操作時(shí)浮声,不對jar包作任何掃描
  2. 首次迭代時(shí),因?yàn)镾erviceLoader.providers中沒有任何緩存旋奢,總是會(huì)通過LazyIterator進(jìn)行懶加載泳挥,并將service實(shí)現(xiàn)的全限定名與加載的service實(shí)例作為key-value緩存到ServiceLoader.providers中。
  3. 之后再進(jìn)行迭代時(shí)至朗,總是在ServiceLoader.providers中進(jìn)行屉符。

3.3 懶加載迭代器LazyIterator

懶加載迭代器LazyIterator主要實(shí)現(xiàn)以下功能:

  1. 首次迭代時(shí),通過ClassLoader.getResources(String)獲得指定services文件的URL集合
  2. 如果是首次遍歷懶加載器,或者對上一個(gè)URL內(nèi)容解析獲得的service實(shí)現(xiàn)類集合完成了迭代矗钟,則從configs中取下一個(gè)services文件URL進(jìn)行解析唆香,按行獲得具體的service實(shí)現(xiàn)類集合,并進(jìn)行迭代吨艇。
  3. 對當(dāng)前URL中解析得到的實(shí)現(xiàn)類集合進(jìn)行迭代躬它,每次返回一個(gè)service實(shí)現(xiàn)類。

下面是LazyIterator的源碼及注釋:

public final class ServiceLoader<S> implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";
    ...

    // Private inner class implementing fully-lazy provider lookup
    private class LazyIterator implements Iterator<S>
    {

        Class<S> service;
        ClassLoader loader;
        Enumeration<URL> configs = null;
        // 當(dāng)前service配置文件的內(nèi)容迭代器
        // 即對services進(jìn)行遍歷东涡,取出一個(gè)services配置文件冯吓,再對該文件按行解析,每行代表一個(gè)具體的service實(shí)現(xiàn)類疮跑,pending是某個(gè)services配置文件中service實(shí)現(xiàn)類的迭代器
        Iterator<String> pending = null;
        String nextName = null;

        private LazyIterator(Class<S> service, ClassLoader loader) {
            this.service = service;
            this.loader = loader;
        }

        private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            // 首次迭代時(shí)桑谍,configs為空,嘗試通過classloader獲取名為:
            // "META-INF/services/[服務(wù)全限定名]"的所有配置文件
            if (configs == null) {
                try {
                    // 注意fullName的定義:"META-INF/services/[服務(wù)全限定名]"
                    String fullName = PREFIX + service.getName();
                    // 通過ClassLoader.getResources()獲得資源URL集合
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            // 如果pending為空祸挪,或者pending已經(jīng)迭代到迭代器末尾锣披,則嘗試解析下一個(gè)services配置文件
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            // 對當(dāng)前pending內(nèi)容進(jìn)行遍歷,每一項(xiàng)代表services的一個(gè)實(shí)現(xiàn)類
            nextName = pending.next();
            return true;
        }
    }

    ...
}

最后贿条,附上parse及parseLine的代碼雹仿,可以發(fā)現(xiàn),parseLine中會(huì)對服務(wù)實(shí)現(xiàn)類進(jìn)行去重整以,所以在一個(gè)或多個(gè)services配置文件中配置多次的服務(wù)實(shí)現(xiàn)類只會(huì)被處理一次胧辽。

public final class ServiceLoader<S> implements Iterable<S>
{
    ...
    // 按行解析給定配置文件。如果解析出的服務(wù)實(shí)現(xiàn)類沒有被其他已解析的配置文件配置過公黑,則通過參數(shù)nams返回給parse方法
    //
    private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
        String ln = r.readLine();
        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            // 去重邑商,防止重復(fù)配置服務(wù),每個(gè)服務(wù)實(shí)現(xiàn)類只會(huì)被解析一次
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

    /**
     * 解析指定的作為SPI配置文件的URL的內(nèi)容
     */
    private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
        InputStream in = null;
        BufferedReader r = null;
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
            try {
                if (r != null) r.close();
                if (in != null) in.close();
            } catch (IOException y) {
                fail(service, "Error closing configuration file", y);
            }
        }
        return names.iterator();
    }
    ...
}

4.JDBC中對SPI的使用

最后凡蚜,以JDBC為例人断,看一個(gè)SPI的實(shí)際使用場景。在文章開始朝蜘,我們提到過恶迈,JDBC4.0之前,我們總是需要在業(yè)務(wù)代碼中顯式地實(shí)例化DB驅(qū)動(dòng)實(shí)現(xiàn)類:

Class.forName("com.mysql.jdbc.Driver");

為什么JDBC4.0之后不需要了呢谱醇?答案就在下面的代碼中暇仲。在系統(tǒng)啟動(dòng)時(shí),DriverManager靜態(tài)初始化時(shí)會(huì)通過ServiceLoader對所有jar包中被注冊為 java.sql.Driver 服務(wù)的驅(qū)動(dòng)實(shí)現(xiàn)類進(jìn)行初始化副渴,這樣就避免了上面通過Class.forName手動(dòng)初始化的繁瑣工作奈附。

public class DriverManager {

    // JDBC驅(qū)動(dòng)注冊中心,所有加載的JDBC驅(qū)動(dòng)都注冊在該CopyOnWriteArrayList中
    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

    ...

    /* Prevent the DriverManager class from being instantiated. */
    private DriverManager(){}

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    private static void loadInitialDrivers() {
        // 如果通過jdbc.drivers配置了驅(qū)動(dòng)煮剧,則在本方法最后進(jìn)行實(shí)例化
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                    public String run() {
                        return System.getProperty("jdbc.drivers");
                    }
                    });
        } catch (Exception ex) {
            drivers = null;
        }
        // If the driver is packaged as a Service Provider, load it.
        // Get all the drivers through the classloader
        // exposed as a java.sql.Driver.class service.
        // ServiceLoader.load() replaces the sun.misc.Providers()
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                // 通過ServiceLoader加載所有通過SPI方式注冊的"java.sql.Driver"服務(wù)
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                // 遍歷ServiceLoader實(shí)例進(jìn)行強(qiáng)制實(shí)例化斥滤,因此除了遍歷不做任何其他操作
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                    // Do nothing
                }
                return null;
                }
            }
        );

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        // 強(qiáng)制加載"jdbc.driver"環(huán)境變量中配置的DB驅(qū)動(dòng)
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }
    ...
}

以mySql驅(qū)動(dòng)為例看看驅(qū)動(dòng)實(shí)例化時(shí)做了什么:

package com.mysql.jdbc;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            // 向DriverManager注冊自己
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     *
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

再看看mysql驅(qū)動(dòng)jar包中對service的配置:

image

因此将鸵,只要某個(gè)驅(qū)動(dòng)以這種方式被引用并被上下文class loader加載,那么該驅(qū)動(dòng)就會(huì)通過SPI的方式被自動(dòng)發(fā)現(xiàn)和加載中跌。實(shí)際使用時(shí)咨堤,Driver.getDriver(url)會(huì)通過DB連接url獲取到正確的驅(qū)動(dòng)并建立與DB的連接。

備注

  1. 本文代碼
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末漩符,一起剝皮案震驚了整個(gè)濱河市一喘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌嗜暴,老刑警劉巖凸克,帶你破解...
    沈念sama閱讀 222,681評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異闷沥,居然都是意外死亡萎战,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,205評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門舆逃,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蚂维,“玉大人,你說我怎么就攤上這事路狮〕嫔叮” “怎么了?”我有些...
    開封第一講書人閱讀 169,421評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵奄妨,是天一觀的道長涂籽。 經(jīng)常有香客問我,道長砸抛,這世上最難降的妖魔是什么评雌? 我笑而不...
    開封第一講書人閱讀 60,114評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮直焙,結(jié)果婚禮上景东,老公的妹妹穿的比我還像新娘。我一直安慰自己箕般,他們只是感情好耐薯,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,116評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著丝里,像睡著了一般。 火紅的嫁衣襯著肌膚如雪体谒。 梳的紋絲不亂的頭發(fā)上杯聚,一...
    開封第一講書人閱讀 52,713評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音抒痒,去河邊找鬼幌绍。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的傀广。 我是一名探鬼主播颁独,決...
    沈念sama閱讀 41,170評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼伪冰!你這毒婦竟也來了誓酒?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,116評(píng)論 0 277
  • 序言:老撾萬榮一對情侶失蹤贮聂,失蹤者是張志新(化名)和其女友劉穎靠柑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吓懈,經(jīng)...
    沈念sama閱讀 46,651評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡歼冰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,714評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了耻警。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片隔嫡。...
    茶點(diǎn)故事閱讀 40,865評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖甘穿,靈堂內(nèi)的尸體忽然破棺而出腮恩,到底是詐尸還是另有隱情,我是刑警寧澤扒磁,帶...
    沈念sama閱讀 36,527評(píng)論 5 351
  • 正文 年R本政府宣布庆揪,位于F島的核電站,受9級(jí)特大地震影響妨托,放射性物質(zhì)發(fā)生泄漏缸榛。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,211評(píng)論 3 336
  • 文/蒙蒙 一兰伤、第九天 我趴在偏房一處隱蔽的房頂上張望内颗。 院中可真熱鬧,春花似錦敦腔、人聲如沸均澳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,699評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽找前。三九已至,卻和暖如春判族,著一層夾襖步出監(jiān)牢的瞬間躺盛,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,814評(píng)論 1 274
  • 我被黑心中介騙來泰國打工形帮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留槽惫,地道東北人周叮。 一個(gè)月前我還...
    沈念sama閱讀 49,299評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像界斜,于是被迫代替她去往敵國和親仿耽。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,870評(píng)論 2 361