Dubbo 扩展机制——SPI

1. 简介

Dubbo 良好的扩展性与两个方面密不可分,一是整个框架中针对不同的场景,恰到好处的使用了各种设计模式,二是接下来要讲的加载机制。基于 Dubbo SPI 加载机制,让整个框架的接口和具体实现类解耦,从而奠定了整个框架良好可扩展性的基础。

2. Java SPI

在讲解 Dubbo SPI 之前,我们先了解下 Java SPI 是怎么使用的。SPI 全称 Service Provider Interface,起初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口多种实现。我们只声明接口,具体的实现并不在程序中直接确定,而是由程序之外的配置掌控,用于具体实现的装配。具体步骤如下:

(1) 定义一个接口及对应的方法。
(2)编写该接口的一个实现类。
(3)在 META-INF/services/ 目录下,创建一个以接口全路径命名的文件,如 com.test.spi.PrintService。
(4)文件内容为具体实现类的全路径名,如果有多个,则用分行符分割。
(5)在代码中通过 java.util.ServiceLoader 来加载所有的实现类。

3. Dubbo SPI

与 Java SPI 相比,Dubbo SPI 做了一定的改进和优化:

  1. JDK 标准的 SPI 会一次性实例化扩展点的所有实现,如果没用上也加载,则浪费资源。而 Dubbo SPI 只是加载配置文件中的类,而不会立即全部初始化。
  2. 增加了对扩展 IOC 和 AOP 的支持,一个扩展可以直接 setter 注入其他扩展。

3.1 代码示例

声明扩展点:

package com.alibaba.dubbo.examples.spi.api;

import com.alibaba.dubbo.common.extension.SPI;

@SPI("print")
public interface PrintService {

    void printInfo();
}

声明扩展点的实现类:

public class PrintServiceImpl implements PrintService {

    @Override
    public void printInfo() {
        System.out.println("hello,world");
    }
}

在 META-INF/dubbo/ 目录下,新建配置文件 com.alibaba.dubbo.examples.spi.api.PrintService,内容如下:

print=com.alibaba.dubbo.examples.spi.impl.PrintServiceImpl

3.2 扩展点的配置规范

Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/ 下放置对应的 SPI 配置文件,文件名称需要命名为接口的全路径名。配置的内容为 key=扩展点实现类的全路径名,如果有多个实现类则使用换行符分隔。其中 key 为 Dubbo SPI 注解中传入的参数。另外,Dubbo SPI 还兼容了 Java SPI 的配置路径,在 Dubbo 启动时,会默认扫描这三个目录下的配置文件:META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal。

3.3 扩展点的特性

从 Dubbo 官方文档中可以知道,扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。

1. 自动包装
自动包装是一种扩展类,ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点实例作为构造函数的参数,则这个扩展类就会被认定为是Wrapper类,例如:

public class ProtocolFilterWrapper implements Protocol {

    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {
        if (protocol == null) {
            throw new IllegalArgumentException("protocol == null");
        }
        this.protocol = protocol;
    }
......
}

ProtocolFilterWrapper继承了Protocol接口,同时其构造函数中又注入了一个Protocol类型的参数。因此ProtocolFilterWrapper会被认定为Wrapper类。这是一种装饰模式,把通用的抽象逻辑封装或对子类进行增强,让子类可以更加专注具体的实现。

2. 自动加载
除了在构造函数中传入其他扩展实例,我们还经常使用setter方法设置属性值,如果某个扩展类是另一个扩展点类的成员属性,并且拥有setter方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader在执行扩展点初始化的时候,会自动通过setter方法注入对应的实现类。这里有个问题,如果扩展类属性是一个接口,他有多种实现,那么具体注入哪个呢?这就涉及第三个特性——自适应。

3. 自适应
在 Dubbo SPI 中,我们使用@Adaptive注解,可以动态地通过 URL 中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。@Adaptive注解使用示例如下:

@SPI("netty")
public interface Transporter {

    @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

@Adaptive传入两个 Constants 中的参数,他们的值分别是"server"、“transporter”。当外部调用Transporter#bind方法时,会动态从传入的参数"URL"中提取 key 参数“server”的 value 值,如果能匹配上某个扩展实现类则直接使用对应的实现类;如果未匹配上,则继续通过第二个 key 参数“transporter”提取 value 的值。如果都未匹配上,则抛出异常。也就是说,如果@Adaptive中传入了多个参数,则依次进行实现类的匹配,直到最后抛出异常。

这种动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要激活多个实现类,如 Filter 可以同时有多个过滤器;根据不同的条件,同时激活多个实现类,如何实现?这就涉及最后一个特性——自动激活。

4. 自动激活
使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同的参数,设置扩展点在不同条件下被自动激活。主要的使用场景是某个扩展点的多个实例需要同时启用(比如 Filter 扩展点)。

3.4 扩展点注解

@SPI注解一般使用在接口上,作用是标记这个接口是一个 Dubbo SPI 接口,既是一个扩展点。注解中有一个 value 属性,表示这个接口的默认实现类。例如上面的@SPI("netty"),我们可以看到Transporter接口使用Netty作为默认实现。

@Adaptive注解可以标注在类、接口、方法上。如果标注在接口的方法上,则可以通过参数动态的获取实现类。方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。例如:Transporter接口在 bind 和 connect 两个方法上添加了@Adaptive注解。Dubbo 在初始化扩展点时,会自动生成一个Transporter$Adaptive类,里面会实现这两个方法,方法里面会有一些通用的抽象逻辑,通过@Adaptive传入的参数,找到并调用真正的实现类。当注解放在实现类上时,则整个实现类会直接作为默认实现,不再自动生成的Adaptive类。在扩展点接口的多个实现里,只能有一个实现类可以加@Adaptive注解,否则抛出异常。

3.5 ExtensionLoader 工作原理

ExtensionLoader是整个扩展机制的主要逻辑类,逻辑入口可以分为getExtensiongetAdaptiveExtensiongetActivateExtension三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活扩展类。

3.5.1 getExtension 的实现原理

当调用getExtension(String name)方法时,会先检查缓存中是否有现成的数据,没有则调用createExtension开始创建。如果 name 为 “true”,则加载并返回默认扩展类。

在调用createExtension开始创建的过程中,也会检查缓存中是否有配置信息,如果不存在扩展类,则会到 META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/ 这几路径中读取所有配置文件,得到对应扩展点实现类的全称。扩展点配置信息加载过程源码如下:

private Map<String, Class<?>> getExtensionClasses() {
        // 尝试先从缓存中获取配置信息
        Map<String, Class<?>> classes = cachedClasses.get();
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    // 缓存中没有,则去配置文件加载
                    classes = loadExtensionClasses();
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

private Map<String, Class<?>> loadExtensionClasses() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if ((value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if (names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                            + ": " + Arrays.toString(names));
                }
                if (names.length == 1) cachedDefaultName = names[0];
            }
        }

        Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
        loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
        loadDirectory(extensionClasses, DUBBO_DIRECTORY);
        loadDirectory(extensionClasses, SERVICES_DIRECTORY);
        return extensionClasses;
    }

检查是否有@SPI注解。如果有,则获取注解中的 value 值,作为扩展点默认实现。然后加载路径下的配置文件loadDirectory

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName();
        try {
            Enumeration<java.net.URL> urls;
            ClassLoader classLoader = findClassLoader();
            if (classLoader != null) {
                urls = classLoader.getResources(fileName);
            } else {
                urls = ClassLoader.getSystemResources(fileName);
            }
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL resourceURL = urls.nextElement();
                    loadResource(extensionClasses, classLoader, resourceURL);
                }
            }
        } catch (Throwable t) {
            logger.error("Exception when load extension class(interface: " +
                    type + ", description file: " + fileName + ").", t);
        }
    }

通过getResources或者getSystemResources得到配置文件,然后遍历并解析配置文件,得到扩展实现类,并加入缓存。加载完扩展点配置后,再通过反射获得所有扩展实现类并缓存起来。注意,此处仅仅是把 Class 加载到 JVM 中,但并没有做 Class 初始化。在加载 Class 文件时,会根据 Class 上的注解来判断扩展点类型,再根据类型分类做缓存,此处逻辑在ExtensionLoader#loadClass

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
         ...
        if (clazz.isAnnotationPresent(Adaptive.class)) {
            // 如果时自适应类,则缓存
            if (cachedAdaptiveClass == null) {
                cachedAdaptiveClass = clazz;
            } else if (!cachedAdaptiveClass.equals(clazz)) {
                // 如果多个自适应实现类,则抛出异常
                throw new IllegalStateException("More than 1 adaptive class found: "
                        + cachedAdaptiveClass.getClass().getName()
                        + ", " + clazz.getClass().getName());
            }
        } else if (isWrapperClass(clazz)) {
            // 如果是包装类(Wrapper),则直接直接加入包装扩展类的 Set 集合
            Set<Class<?>> wrappers = cachedWrapperClasses;
            if (wrappers == null) {
                cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
                wrappers = cachedWrapperClasses;
            }
            wrappers.add(clazz);
        } else {
            // 不是自适应类,也不是包装类,只剩下普通扩展类了
            clazz.getConstructor();
            if (name == null || name.length() == 0) {
                name = findAnnotationName(clazz);
                if (name.length() == 0) {
                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
                }
            }
            String[] names = NAME_SEPARATOR.split(name);
            if (names != null && names.length > 0) {
                Activate activate = clazz.getAnnotation(Activate.class);
                if (activate != null) {
                    // 如果扩展类有 @Activate 注解,则加入自动激活类缓存
                    cachedActivates.put(names[0], activate);
                }
                for (String n : names) {
                    if (!cachedNames.containsKey(clazz)) {
                        cachedNames.put(clazz, n);
                    }
                    Class<?> c = extensionClasses.get(n);
                    if (c == null) {
                        // 加入普通扩展类缓存
                        extensionClasses.put(n, clazz);
                    } else if (c != clazz) {
                        throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                    }
                }
            }
        }
    }

加载完毕扩展信息之后,完成了createExtension的第一步:

 private T createExtension(String name) {
        // 根据 key 获取对应的扩展点实现类
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw findException(name);
        }
        try {
            T instance = (T) EXTENSION_INSTANCES.get(clazz);
            if (instance == null) {
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            }
            // 依赖注入
            injectExtension(instance);
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                // 处理包装扩展类
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

下一步就是根据传入的 name 找到对应的类并通过Class.forName方法进行初始化,并为其注入依赖的其他扩展类(自动加载特性),我们看injectExtension

private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                for (Method method : instance.getClass().getMethods()) {
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                       // 找到 setter 方法
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        Class<?> pt = method.getParameterTypes()[0];
                        try {
                            // 通过字符串截取,获得小写开头的类名。如 setTestService,截取之后是 testService
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            // 根据 key 获取对应的扩展点实例
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }

injectExtension方法体实现了类似 Spring 的 IoC 机制,其实现原理比较简单:首先通过反射获取类的所有方法,然后遍历以字符串“set”开头的方法,得到 set 方法的参数类型,再通过ExtensionFactory寻找参数类型相同的扩展类实例,如果找到就设置进去。

当扩展类初始化之后,会检查一次包装扩展类Set<Class<?>> cachedWrapperClasses,查找包含与扩展点类型相同的构造函数,为其注入刚初始化的扩展类。从源码可知,包装类的构造函数注入也是通过injectExtension方法实现的。

3.5.2 getAdaptiveExtension 的实现原理

由之前的流程我们可以知道,getAdaptiveExtension()方法中,会为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:为接口中每个有@Adaptive注解的方法生成默认实现(没有注解的方法则生成空实现),每个默认实现都会从 URL 中提取@Adaptive参数值,并且以此为依据动态加载扩展点。然后框架会使用不同的编译器,把实现类字符串编译为自适应类并返回。

如果一个接口上既有@SPI("impl")注解,方法上又有@Adaptive("impl2")注解,那么会使用哪个 key 作为默认实现呢?其优先通过@Adaptive注解传入的 key 去查找扩展实现类;如果没有找到,则通过@SPI注解中 key 去查找;如果@SPI注解中没有默认值,则会采用驼峰规则,把类名转化为 key,再去查找。

驼峰规则:比如类名为 SimpleExt,则转化后的 key 为 simple.ext。

3.5.3 getActivateExtension 的实现原理

由于@Activate使用场景较少,并且实现原理较为简单,感兴趣的同学自行去了解。

3.6 ExtensionFactory 的实现原理

ExtensionFactory类似于Spring中的BeanFactory,它是扩展类的 bean 工厂。我们看下它的实现类:


既然工厂接口有多个实现,那么是怎么确定使用哪个工厂实现的呢?我们可以看到AdaptiveExtensionFactory这个实现类工厂上有@Adaptive注解。因此,AdaptiveExtensionFactory会作为默认实现类。

除了AdaptiveExtensionFactory,还有SpiExtensionFactorySpringExtensionFactory两个工厂。也就是说,我们除了可以从 Dubbo SPI 管理的容器中获取扩展点实例,还可以从 Spring 容器获取。

我们先看SpringExtensionFactory

public class SpringExtensionFactory implements ExtensionFactory {

    private static final Set<ApplicationContext> contexts = new ConcurrentHashSet<ApplicationContext>();

    public static void addApplicationContext(ApplicationContext context) {
        contexts.add(context);
        BeanFactoryUtils.addApplicationListener(context, shutdownHookListener);
    }
    ...
    @Override
    @SuppressWarnings("unchecked")
    public <T> T getExtension(Class<T> type, String name) {
        // 先根据名称去获取
        for (ApplicationContext context : contexts) {
            if (context.containsBean(name)) {
                Object bean = context.getBean(name);
                if (type.isInstance(bean)) {
                    return (T) bean;
                }
            }
        }
        // 再根据类型去获取
        for (ApplicationContext context : contexts) {
            try {
                return context.getBean(type);
            } catch (NoUniqueBeanDefinitionException multiBeanExe) {
                logger.warn("Find more than 1 spring extensions (beans) of type " + type.getName() + ", will stop auto injection. Please make sure you have specified the concrete parameter type and there's only one extension of that type.");
            } catch (NoSuchBeanDefinitionException noBeanExe) {
                if (logger.isDebugEnabled()) {
                    logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe);
                }
            }
        }

        logger.warn("No spring extension (bean) named:" + name + ", type:" + type.getName() + " found, stop get bean.");

        return null;
    }

}

该工厂提供了保存 Spring 上下文的静态方法,可以把 Spring 上下文保存在 Set 集合中,当调用getExtension获取扩展类时,会遍历 Set 集合中的所有 Spring 上下,先根据名字依次去获取,如果没有获取到,再根据类型去获取。

那么 Spring 的上下文又是什么时候被保存的呢?我们可以通过代码搜索得知,在ReferenceBeanServiceBean中会调用静态方法保存 Spring 上下文,即一个服务被发布或者被引用的时候,对应的 Spring 上下文会被保存下来。

我们再看SpiExtensionFactory:

public class SpiExtensionFactory implements ExtensionFactory {

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
            if (!loader.getSupportedExtensions().isEmpty()) {
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }

}

主要是获取扩展点接口对应的 Adaptive 实现类。例如:某个扩展点实现类 ClassA 上有@Adaptive注解,则使用SpiExtensionFactory#getExtension会直接返回 ClassA 实例。

我们再看AdaptiveExtensionFactory:

/**
 * AdaptiveExtensionFactory
 * 该类的作用是管理其他的 ExtensionFactory
 */
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {

    private final List<ExtensionFactory> factories;

    /**
     * 构造方法会加载其他扩展工厂
     */
    public AdaptiveExtensionFactory() {
        ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
        List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
        /*
         * 获取 getExtensionClasses() 是不会加载被 @Adaptive 注解的实现类的
         * 即这里只是把其他两个 ExtensionFactory 放入 factories 中
         */
        for (String name : loader.getSupportedExtensions()) {
            list.add(loader.getExtension(name));
        }
        factories = Collections.unmodifiableList(list);
    }

    @Override
    public <T> T getExtension(Class<T> type, String name) {
        for (ExtensionFactory factory : factories) {
            T extension = factory.getExtension(type, name);
            if (extension != null) {
                return extension;
            }
        }
        return null;
    }

}

这个默认工厂在构造方法中就获取了其他所有扩展类工厂并缓存起来,包括SpringExtensionFactorySpiExtensionFactory。被AdaptiveExtensionFactory缓存的工厂会通过TreeSet进行自然排序,SPI 排在前面,Spring 排在后面。当调用getExtension方法时,会遍历所有的工厂,先从 SPI 容器获取扩展类;如果没有找到,再从 Spring 容器中查找。我们可以理解为,AdaptiveExtensionFactory持有了所有的工厂实现,它的getExtension方法只是遍历它持有的所有工厂,最终还是调用 SPI 或者 Spring 工厂实现的getExtension方法。

4. 小结

我们没有讲扩展点的动态编译,其实现手法跟ExtensionFactory类似,其采用含有@Adaptive注解的AdaptiveCompiler作为Compiler的默认实现,主要作用是为了管理其他Compiler(JavassistCompilerJdkCompiler)。由于Compiler接口上采用@SPI("javassist"),说明 Javassist 编译器作为默认编译器。

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

推荐阅读更多精彩内容