apache-shenyu之SPI

從這篇文章開始會(huì)從頭開始以 apache shenyu為路徑,一一學(xué)習(xí)shenyu中用到的技術(shù)及設(shè)計(jì)第步,當(dāng)前文章學(xué)習(xí)shenyu中的spi(apache-shenyu 2.4.3版本)

apache shenyu前身soul網(wǎng)關(guān),是一款java中spring5新引入的project-reactor的webflux武学,reactor-netty等為基礎(chǔ)實(shí)現(xiàn)的高性能網(wǎng)關(guān)啥酱,現(xiàn)已進(jìn)入apache孵化器仔拟,作者yu199195 (xiaoyu) (github.com)

作者也是國內(nèi)知名開源社區(qū)dromara的創(chuàng)始人合是,并且作有多個(gè)開源產(chǎn)品了罪,apache-shenyu是其中之一apache/incubator-shenyu: ShenYu is High-Performance Java API Gateway. (github.com)

SPI是java多態(tài)和插件化非常重要的一環(huán)。

簡(jiǎn)單的例子聪全,java ee中jdbc的數(shù)據(jù)庫驅(qū)動(dòng)泊藕,我們可以任意切換連接的數(shù)據(jù)庫,例如mysql难礼,oracle等等娃圆,但是對(duì)于你的java應(yīng)用來說并不知道具體使用哪個(gè)數(shù)據(jù)庫汽久,而jdbc將對(duì)于數(shù)據(jù)庫的操作抽象出一套標(biāo)準(zhǔn)的接口,jdbc對(duì)于數(shù)據(jù)庫的操作基于接口來進(jìn)行編碼踊餐,而不需要知道具體的實(shí)現(xiàn),但是不同數(shù)據(jù)庫的語法臀稚,實(shí)現(xiàn)邏輯都不相同吝岭,但是他們會(huì)根據(jù)jdbc提供的標(biāo)準(zhǔn)接口實(shí)現(xiàn)驅(qū)動(dòng)程序,那么在java應(yīng)用側(cè)只需要在使用哪個(gè)數(shù)據(jù)庫時(shí)吧寺,指定好driver-class-name即可窜管。
jdbc抽象的主要是Driver接口和Connection接口


image.png

以上為不同數(shù)據(jù)庫廠商提供的Driver實(shí)現(xiàn),jdbc不需要知道如何實(shí)現(xiàn)稚机,只需要使用Driver接口編碼幕帆,然后由配置指定其子類,這就是一種SPI思想

public interface Driver {
// 不同的driver實(shí)現(xiàn)拿到不同的連接
    Connection connect(String url, java.util.Properties info)
        throws SQLException;

    boolean acceptsURL(String url) throws SQLException;
    DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
                         throws SQLException;
    int getMajorVersion();
    int getMinorVersion();
    boolean jdbcCompliant();
    //------------------------- JDBC 4.1 -----------------------------------
    public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}
Connection接口方法

Connection接口方法

還有一堆方法赖条,感興趣的直接看java.sql.Connection接口失乾,那么jdbc將連接的各種動(dòng)作,和數(shù)據(jù)庫交互的邏輯抽象為對(duì)應(yīng)方法纬乍,那么jdbc直接使用這些方法就可以了碱茁,實(shí)現(xiàn)則由數(shù)據(jù)庫那邊不同的實(shí)現(xiàn)通過SPI注入。
當(dāng)然如果通過指定子類名稱仿贬,可以通過反射直接創(chuàng)建其實(shí)例注入纽竣。
下面介紹如果不知道子類名稱的情況下的spi實(shí)現(xiàn)。

java原生SPI茧泪,使用ServiceLoader蜓氨,METAINF/services實(shí)現(xiàn)

具體點(diǎn)大家可以繼續(xù)搜索學(xué)習(xí),這里只講解使用方法

ServiceLoader + META-INF/services 的使用方法

我們可以通過ServiceLoader#load方法來加載子類队伟。然后通過services文件夾中命名接口/抽象類內(nèi)部寫入要load的實(shí)現(xiàn)類名稱穴吹,其實(shí)也可以通過classLoader將當(dāng)前classpath所有類變量判斷是否是其子類判斷出要找的類

  1. 如果有多個(gè)實(shí)現(xiàn)類,我們就要指定具體某個(gè)實(shí)現(xiàn)類缰泡,那就要么在代碼邏輯寫死或者又引入新的配置邏輯
  2. 性能很差
    所有java提供了SPI機(jī)制刀荒,快速并且解耦的實(shí)現(xiàn)了選擇性子類發(fā)現(xiàn)機(jī)制
舉例com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy是hystrix中關(guān)于線程執(zhí)行相關(guān)的策略抽象。是一個(gè)抽象類(接口也可以)
services指定其被抽象的類

我們可以使用自己的實(shí)現(xiàn)棘钞,通過SPI機(jī)制


我們可以使用自己的實(shí)現(xiàn)缠借,通過SPI機(jī)制
實(shí)現(xiàn)當(dāng)然也可以多個(gè),但是SPI機(jī)制大部分用來實(shí)現(xiàn)多種實(shí)現(xiàn)宜猜,通過配置或者參數(shù)來選擇一個(gè)實(shí)現(xiàn)使用泼返,多個(gè)實(shí)現(xiàn)可以自由替換,上面的例子就是 jdbc抽象的Driver和Connection接口通過 driver-class-name來選擇姨拥,

當(dāng)然也可以是動(dòng)態(tài)切換绅喉,例如下面要看的基于dubbo的spi方式


也可以多個(gè)

hystrix源碼

    private static <T> T findService(
            Class<T> spi, 
            ClassLoader classLoader) throws ServiceConfigurationError {
        // 這里加載出來是一個(gè)可迭代的集合渠鸽,所以是可以放入多個(gè)實(shí)現(xiàn)
        ServiceLoader<T> sl = ServiceLoader.load(spi,
                classLoader);
        for (T s : sl) {
// hystrix的這個(gè)抽象是直接返回第一個(gè)實(shí)現(xiàn)
            if (s != null)
                return s;
        }
        return null;
    }
   private <T> T getPluginImplementation(Class<T> pluginClass) {
// 這里是通過配置文件的 全類名,通過反射實(shí)例化柴罐,這也是一種常見的抽象機(jī)制
        T p = getPluginImplementationViaProperties(pluginClass, dynamicProperties);
        if (p != null) return p;        
// 利用java的SPI指定子類,然后就會(huì)使用其實(shí)現(xiàn)處理業(yè)務(wù)
        return findService(pluginClass, classLoader);
    }

shenyu的SPI徽缚,是參照了dubbo的spi實(shí)現(xiàn),但是本文只閱讀apache-shenyu的設(shè)計(jì)以及解決的問題

由一個(gè) @SPI注解開始

/**
 * SPI Extend the processing.
 * All spi system reference the apache implementation of
 * <a >Apache Dubbo Common Extension</a>.
 *
 * @see ExtensionFactory
 * @see ExtensionLoader
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
    /**
     * Value string.
     *
     * @return the string
     */
    String value() default "";
}
使用注解的接口們
舉例一個(gè)場(chǎng)景革屠,apache-shenyu支持多種rule(即后端業(yè)務(wù)服務(wù)的url或者規(guī)則)注冊(cè)方式凿试,也就是業(yè)務(wù)服務(wù)有哪些接口可以通過各種方式上報(bào)給shenyu-admin服務(wù),然后shenyu-admin會(huì)把數(shù)據(jù)同步給shenyu-bootstrap服務(wù)(真正做網(wǎng)關(guān)的服務(wù))分別提供了似芝,nacos那婉,http,consul党瓮,etcd详炬,zookeeper多種rule上報(bào)實(shí)現(xiàn),只需要通過配置選擇即可寞奸。那么在shenyu的邏輯代碼中只需要對(duì)抽象出來的ShenyuClientServerRegisterRepository接口統(tǒng)一處理呛谜,不需要知道不同上報(bào)方式的差別,這就是java的抽象方式枪萄,通過接口或者抽象類(盡量使用接口)的方式抽象后不需要if呻率,switch來判斷了,這種只要選擇一個(gè)實(shí)現(xiàn)的邏輯使用SPI機(jī)制精簡(jiǎn)了代碼呻引,解耦了模塊間的依賴礼仗,一下圖中多種上報(bào)實(shí)現(xiàn)只需要在實(shí)現(xiàn)類中關(guān)注
上報(bào)方式

apache-shenyu中很多這種SPI抽象邏輯,例如還有RateLimiterAlgorithm接口將限流邏輯抽象逻悠,
限流實(shí)現(xiàn)

下面看代碼


shenyu的spi代碼
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
  // 指定在抽象方元践,也就放到接口上相當(dāng)于jdk中的META-INF/services文件的名稱
    /**
     * Value string.
     * 這里可以不使用 META-INF/shenyu路徑中的文件指定其實(shí)現(xiàn),直接通過注解童谒,多一種切換方式单旁,當(dāng)然這里的spi加載也可以多實(shí)現(xiàn),下面代碼會(huì)提到
     * @return the string
     */
    String value() default "";
}
@SPI
public interface ShenyuClientServerRegisterRepository {
// 省略代碼
}

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Join {
// 放到實(shí)現(xiàn)類上饥伊,相當(dāng)于jdk中的META-INF/services文件中的配置
}

ExtensionFactory spi工廠象浑,那么spi的實(shí)現(xiàn)也可以使用多個(gè)實(shí)現(xiàn),但是apache-shenyu目前只有一個(gè)spi工廠,這里想看spi工廠多實(shí)現(xiàn)可以找dubbo源碼看一看

@SPI("spi")
public interface ExtensionFactory {

    /**
     * Gets Extension.
     *
     * @param <T>   the type parameter
     * @param key   the key
     * @param clazz the clazz
     * @return the extension
     */
    <T> T getExtension(String key, Class<T> clazz);
}
@Join
public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    public <T> T getExtension(final String key, final Class<T> clazz) {
        return Optional.ofNullable(clazz)
                .filter(Class::isInterface)
                .filter(cls -> cls.isAnnotationPresent(SPI.class))
                .map(ExtensionLoader::getExtensionLoader)
                .map(ExtensionLoader::getDefaultJoin)
                .orElse(null);
    }
}
dubbo的spi是一個(gè)kv

下面看看核心代碼ExtensionLoader

@SuppressWarnings("all")
public final class ExtensionLoader<T> {
    
    private static final Logger LOG = LoggerFactory.getLogger(ExtensionLoader.class);
    // 學(xué)習(xí)jdk的spi機(jī)制指定實(shí)現(xiàn)方式
    private static final String SHENYU_DIRECTORY = "META-INF/shenyu/";
    // key為抽象的類的class對(duì)象琅豆,value為當(dāng)前類對(duì)象的實(shí)例愉豺,使用泛型機(jī)制,在實(shí)例化時(shí)類型已經(jīng)確定茫因,保證類型安全蚪拦,這里針對(duì)不同的抽象,使用各自的Loader類
    private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
    // 抽象出來的接口的class對(duì)象
    private final Class<T> clazz;
    
    private final ClassLoader classLoader;
    // 緩存的 實(shí)現(xiàn)類class,實(shí)現(xiàn)類的key(dubbo的spi可以設(shè)置kv映射) -> 實(shí)現(xiàn)類class對(duì)象驰贷,一個(gè)holder對(duì)象緩存當(dāng)前抽象接口的所有子類class
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    // 緩存的實(shí)現(xiàn)類 實(shí)例盛嘿,實(shí)現(xiàn)類key -> 實(shí)現(xiàn)類的實(shí)例對(duì)象,一個(gè)holder對(duì)象緩存一個(gè) 實(shí)例
    private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    // key為實(shí)現(xiàn)類class對(duì)象括袒,value為其實(shí)例
    private final Map<Class<?>, Object> joinInstances = new ConcurrentHashMap<>();
    
    private String cachedDefaultName;
    
    /**
     * Instantiates a new Extension loader.
     *
     * @param clazz the clazz.
     */
    private ExtensionLoader(final Class<T> clazz, final ClassLoader cl) {
//一個(gè)在內(nèi)部實(shí)例化其 傳入的抽象接口class對(duì)象的ExtensionLoader
        this.clazz = clazz;
        this.classLoader = cl;
        if (!Objects.equals(clazz, ExtensionFactory.class)) {
            ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();
        }
    }
    // 暴露出去的唯二static方法之一次兆,區(qū)別需要傳入classLoader,基本都是用另外一個(gè)锹锰,其他方法通過實(shí)例調(diào)用
    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz, final ClassLoader cl) {
        
        Objects.requireNonNull(clazz, "extension clazz is null");
        // 可以看到這里的SPI限制必須使用接口类垦,接口可以多繼承,相對(duì)抽象類還是好用一些的城须。
        if (!clazz.isInterface()) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
        }
// 必須是@SPI注解的接口
        if (!clazz.isAnnotationPresent(SPI.class)) {
            throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + " Annotation");
        }
// 獲取對(duì)應(yīng) 抽象接口的ExtensionLoader
        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
        if (Objects.nonNull(extensionLoader)) {
            return extensionLoader;
        }
// 如果沒有調(diào)用過實(shí)例化一個(gè)ExtensionLoader,看來是懶加載
        LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz, cl));
        return (ExtensionLoader<T>) LOADERS.get(clazz);
    }
    
// 暴露出去的唯二static方法米苹,大部分使用這個(gè)糕伐,其他方法通過實(shí)例調(diào)用
    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {
        return getExtensionLoader(clazz, ExtensionLoader.class.getClassLoader());
    }
    
   // 通過實(shí)現(xiàn)類key獲取其實(shí)例
    public T getDefaultJoin() {
        getExtensionClasses();
        if (StringUtils.isBlank(cachedDefaultName)) {
            return null;
        }
        return getJoin(cachedDefaultName);
    }
    
   // 通過實(shí)現(xiàn)類key獲取其實(shí)例
    public T getJoin(final String name) {
        if (StringUtils.isBlank(name)) {
            throw new NullPointerException("get join name is null");
        }
        Holder<Object> objectHolder = cachedInstances.get(name);
        if (Objects.isNull(objectHolder)) {
// 第一次獲取其實(shí)例,放入一個(gè)holder容器
            cachedInstances.putIfAbsent(name, new Holder<>());
            objectHolder = cachedInstances.get(name);
        }
        Object value = objectHolder.getValue();
// 通過雙重校驗(yàn) 保證單例
        if (Objects.isNull(value)) {
            synchronized (cachedInstances) {
                value = objectHolder.getValue();
                if (Objects.isNull(value)) {
// 創(chuàng)建要獲取的實(shí)例
                    value = createExtension(name);
                    objectHolder.setValue(value);
                }
            }
        }
        return (T) value;
    }
    
// 獲取所有實(shí)現(xiàn)蘸嘶,為什么沒有參數(shù)良瞧,因?yàn)閷?duì)于當(dāng)前類的實(shí)例只會(huì)對(duì)應(yīng)一個(gè)
// 抽象的接口,和其所有實(shí)現(xiàn)類的實(shí)例緩存训唱,如果前面獲取到了抽象接口的ExtensionLoader則直接可獲取所有實(shí)現(xiàn)的實(shí)例
    public List<T> getJoins() {
// 獲取所有實(shí)現(xiàn)類的緩存褥蚯,如果第一次獲取,會(huì)將所有class對(duì)象加載并緩存
        Map<String, Class<?>> extensionClasses = this.getExtensionClasses();
        if (extensionClasses.isEmpty()) {
            return Collections.emptyList();
        }
// 如果剛加載的所有class子類實(shí)現(xiàn)的對(duì)象與其實(shí)現(xiàn)類的實(shí)例數(shù)量况增,說明所有實(shí)現(xiàn)class對(duì)象已經(jīng)都實(shí)例化了直接返回
        if (Objects.equals(extensionClasses.size(), cachedInstances.size())) {
            return (List<T>) this.cachedInstances.values().stream().map(e -> {
                return e.getValue();
            }).collect(Collectors.toList());
        }
        List<T> joins = new ArrayList<>();
        extensionClasses.forEach((name, v) -> {
// 如果哪些class沒有實(shí)例化赞庶,進(jìn)行實(shí)例化
            T join = this.getJoin(name);
            joins.add(join);
        });
        return joins;
    }
    
    @SuppressWarnings("unchecked")
    private T createExtension(final String name) {
// 獲取子類實(shí)現(xiàn)的class對(duì)象
        Class<?> aClass = getExtensionClasses().get(name);
        if (Objects.isNull(aClass)) {
            throw new IllegalArgumentException("name is error");
        }
        Object o = joinInstances.get(aClass);
        if (Objects.isNull(o)) {
            try {
// 這里只通過concurrentMap + putIfAbsent保證線程安全,實(shí)例化出來多個(gè)無所謂澳骤,只會(huì)成功放入第一個(gè)歧强,也是單例的。
                joinInstances.putIfAbsent(aClass, aClass.newInstance());
                o = joinInstances.get(aClass);
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalStateException("Extension instance(name: " + name + ", class: "
                        + aClass + ")  could not be instantiated: " + e.getMessage(), e);
                
            }
        }
        return (T) o;
    }
    
// class對(duì)象必須保證只加載一次为肮,也就是一個(gè)抽象接口的所有子類class只會(huì)加載一次
    public Map<String, Class<?>> getExtensionClasses() {
        Map<String, Class<?>> classes = cachedClasses.getValue();
// class對(duì)象必須保證只加載一次摊册,通過雙重校驗(yàn)
        if (Objects.isNull(classes)) {
            synchronized (cachedClasses) {
                classes = cachedClasses.getValue();
                if (Objects.isNull(classes)) {
                    classes = loadExtensionClass();
                    cachedClasses.setValue(classes);
                }
            }
        }
        return classes;
    }
    // 加載子類 class
    private Map<String, Class<?>> loadExtensionClass() {
        SPI annotation = clazz.getAnnotation(SPI.class);
        if (Objects.nonNull(annotation)) {
            String value = annotation.value();
            if (StringUtils.isNotBlank(value)) {
                cachedDefaultName = value;
            }
        }
        Map<String, Class<?>> classes = new HashMap<>(16);
        loadDirectory(classes);
        return classes;
    }
    
    // 加載子類 class
    private void loadDirectory(final Map<String, Class<?>> classes) {
        String fileName = SHENYU_DIRECTORY + clazz.getName();
        try {
            Enumeration<URL> urls = Objects.nonNull(this.classLoader) ? classLoader.getResources(fileName)
                    : ClassLoader.getSystemResources(fileName);
            if (Objects.nonNull(urls)) {
                while (urls.hasMoreElements()) {
                    URL url = urls.nextElement();
                    loadResources(classes, url);
                }
            }
        } catch (IOException t) {
            LOG.error("load extension class error {}", fileName, t);
        }
    }
    
    private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {
        try (InputStream inputStream = url.openStream()) {
            Properties properties = new Properties();
            properties.load(inputStream);
            properties.forEach((k, v) -> {
// dubbo的spi形式不同于jdk,是http=org.apache.shenyu.admin.controller.ShenyuClientHttpRegistryController的形式颊艳,左邊為key茅特,右邊為類名,可以維護(hù)一個(gè)map形式的子類實(shí)現(xiàn)集合
                String name = (String) k;
                String classPath = (String) v;
                if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
                    try {
                        loadClass(classes, name, classPath);
                    } catch (ClassNotFoundException e) {
                        throw new IllegalStateException("load extension resources error", e);
                    }
                }
            });
        } catch (IOException e) {
            throw new IllegalStateException("load extension resources error", e);
        }
    }
    
    private void loadClass(final Map<String, Class<?>> classes,
                           final String name, final String classPath) throws ClassNotFoundException {
//獲取子類實(shí)現(xiàn)的class對(duì)象
        Class<?> subClass = Objects.nonNull(this.classLoader) ? Class.forName(classPath, true, this.classLoader) : Class.forName(classPath);
// 校驗(yàn)必須為其子類
        if (!clazz.isAssignableFrom(subClass)) {
            throw new IllegalStateException("load extension resources error," + subClass + " subtype is not of " + clazz);
        }
// 校驗(yàn)必須標(biāo)注 @Join注解
        if (!subClass.isAnnotationPresent(Join.class)) {
            throw new IllegalStateException("load extension resources error," + subClass + " without @" + Join.class + " annotation");
        }
        Class<?> oldClass = classes.get(name);
        if (Objects.isNull(oldClass)) {
//放入
            classes.put(name, subClass);
        } else if (!Objects.equals(oldClass, subClass)) {
// 如果產(chǎn)生了重復(fù)放入棋枕,校驗(yàn)是否相同白修,不同報(bào)錯(cuò)
            throw new IllegalStateException("load extension resources error,Duplicate class " + clazz.getName() + " name " + name + " on " + oldClass.getName() + " or " + subClass.getName());
        }
    }
    
    /**
     * The type Holder.
     *
     * @param <T> the type parameter.
     */
//用于緩存的包裝類,可能緩存子類實(shí)現(xiàn)類的實(shí)例對(duì)象重斑,或者所有子類的class對(duì)象map
    public static class Holder<T> {
        // 使用volatile熬荆,保證其他線程可見性
        private volatile T value;
        
        /**
         * Gets value.
         *
         * @return the value
         */
        public T getValue() {
            return value;
        }
        /**
         * Sets value.
         *
         * @param value the value
         */
        public void setValue(final T value) {
            this.value = value;
        }
    }
}

總結(jié)

  1. apache-shenyu基本沿用dubbo的spi機(jī)制,通過@SPI,@Join + META-INF/指定名稱 的目錄加載卤恳,但是文件內(nèi)容使用kv形式累盗,也提供多實(shí)現(xiàn)通過文件配置中的不同key區(qū)別
  2. 有一個(gè)spi工廠提供可能存在更多的spi實(shí)現(xiàn)
  3. 使用泛型保證類型安全,不同的抽象父類突琳,實(shí)例化出不同的ExtensionLoader加載器來處理
  4. 使用緩存若债,線程安全容器,雙重校驗(yàn)等保證單例
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末拆融,一起剝皮案震驚了整個(gè)濱河市蠢琳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌镜豹,老刑警劉巖傲须,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異趟脂,居然都是意外死亡泰讽,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門昔期,熙熙樓的掌柜王于貴愁眉苦臉地迎上來已卸,“玉大人,你說我怎么就攤上這事硼一±墼瑁” “怎么了?”我有些...
    開封第一講書人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵般贼,是天一觀的道長愧哟。 經(jīng)常有香客問我,道長哼蛆,這世上最難降的妖魔是什么翅雏? 我笑而不...
    開封第一講書人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮人芽,結(jié)果婚禮上望几,老公的妹妹穿的比我還像新娘。我一直安慰自己萤厅,他們只是感情好橄抹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著惕味,像睡著了一般楼誓。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上名挥,一...
    開封第一講書人閱讀 51,208評(píng)論 1 299
  • 那天疟羹,我揣著相機(jī)與錄音,去河邊找鬼。 笑死榄融,一個(gè)胖子當(dāng)著我的面吹牛参淫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播愧杯,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼涎才,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了力九?” 一聲冷哼從身側(cè)響起耍铜,我...
    開封第一講書人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎跌前,沒想到半個(gè)月后棕兼,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡抵乓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年伴挚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片臂寝。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖摊灭,靈堂內(nèi)的尸體忽然破棺而出咆贬,到底是詐尸還是另有隱情,我是刑警寧澤帚呼,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布掏缎,位于F島的核電站,受9級(jí)特大地震影響煤杀,放射性物質(zhì)發(fā)生泄漏眷蜈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一沈自、第九天 我趴在偏房一處隱蔽的房頂上張望酌儒。 院中可真熱鬧,春花似錦枯途、人聲如沸忌怎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽榴啸。三九已至晚岭,卻和暖如春鸥印,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來泰國打工库说, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留狂鞋,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓璃弄,卻偏偏與公主長得像要销,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子夏块,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容