Tomcat启动分析(二) - Catalina类

Tomcat启动分析(一)的末尾提到Bootstrap类利用反射实例化Catalina类后接着调用Catalina类的load方法,本文先从load方法开始分析Catalina类,然后再分析start方法。

load方法

Catalina类的load方法代码如下:

public void load() {
    if (loaded) {
        return;
    }
    loaded = true;
    long t1 = System.nanoTime();
    initDirs();
    // Before digester - it may be needed
    initNaming();
    // Create and execute our Digester
    Digester digester = createStartDigester();
    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
        try {
            file = configFile();
            inputStream = new FileInputStream(file);
            inputSource = new InputSource(file.toURI().toURL().toString());
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("catalina.configFail", file), e);
            }
        }
        // 省略一些代码
        try {
            inputSource.setByteStream(inputStream);
            digester.push(this);
            digester.parse(inputSource);
        } catch (SAXParseException spe) {
            log.warn("Catalina.start using " + getConfigFile() + ": " +
                    spe.getMessage());
            return;
        } catch (Exception e) {
            log.warn("Catalina.start using " + getConfigFile() + ": " , e);
            return;
        }
    } finally {
        // 省略一些代码
    }
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    // Stream redirection
    initStreams();
    // Start the new server
    try {
        getServer().init();
    } catch (LifecycleException e) {
        if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
            throw new java.lang.Error(e);
        } else {
            log.error("Catalina.start", e);
        }
    }
    long t2 = System.nanoTime();
    if(log.isInfoEnabled()) {
        log.info("Initialization processed in " + ((t2 - t1) / 1000000) + " ms");
    }
}

简单地说,load方法主要做了以下几件事:

  • initDirs方法初始化目录,检查java.io.tmdir系统属性是否存在;
  • initNaming方法初始化命名,这个不清楚做什么的,好像是与JNDI有关的;
  • createStartDigester方法创建Digester解析conf/server.xml文件,digester.parse(inputSource)开始真正的解析过程;
  • 为Server实例设置Tomcat安装目录和工作目录;
  • initStreams方法设置新的标准输出和标准错误输出;
  • Server实例初始化。

下面详细分析上述每项工作。

initDirs方法

initDirs方法很简单,就是检查一下java.io.tmpdir属性表示的是否是一个存在的目录:

protected void initDirs() {
    String temp = System.getProperty("java.io.tmpdir");
    if (temp == null || (!(new File(temp)).isDirectory())) {
        log.error(sm.getString("embedded.notmp", temp));
    }
}

initNaming方法

TODO,暂时略过,不太懂JNDI。

解析server.xml文件

因为笔者以前做过和XML解析有关的工作,所以在load方法中看到InputSource后就意识到该部分代码与XML有关。解析工作由Digester类完成,该类的实例由createStartDigester方法创建,解析入口在digester.parse(inputSource)这一行。Digester类一边解析XML,一边利用反射实例化各个组件并建立组件之间的关系。实际上,Apache有一个专门解析XML的Digester项目,感兴趣的读者可以深入了解。
Catalina类的createStartDigester方法部分代码如下:

protected Digester createStartDigester() {
    long t1=System.currentTimeMillis();
    // Initialize the digester
    Digester digester = new Digester();
    digester.setValidating(false);
    digester.setRulesValidation(true);
    HashMap<Class<?>, List<String>> fakeAttributes = new HashMap<>();
    ArrayList<String> attrs = new ArrayList<>();
    attrs.add("className");
    fakeAttributes.put(Object.class, attrs);
    digester.setFakeAttributes(fakeAttributes);
    digester.setUseContextClassLoader(true);

    // Configure the actions we will be using
    digester.addObjectCreate("Server",
                                "org.apache.catalina.core.StandardServer",
                                "className");
    digester.addSetProperties("Server");
    digester.addSetNext("Server",
                        "setServer",
                        "org.apache.catalina.Server");

    digester.addObjectCreate("Server/GlobalNamingResources",
                                "org.apache.catalina.deploy.NamingResourcesImpl");
    digester.addSetProperties("Server/GlobalNamingResources");
    digester.addSetNext("Server/GlobalNamingResources",
                        "setGlobalNamingResources",
                        "org.apache.catalina.deploy.NamingResourcesImpl");

    digester.addObjectCreate("Server/Listener",
                                null, // MUST be specified in the element
                                "className");
    digester.addSetProperties("Server/Listener");
    digester.addSetNext("Server/Listener",
                        "addLifecycleListener",
                        "org.apache.catalina.LifecycleListener");

    digester.addObjectCreate("Server/Service",
                                "org.apache.catalina.core.StandardService",
                                "className");
    digester.addSetProperties("Server/Service");
    digester.addSetNext("Server/Service",
                        "addService",
                        "org.apache.catalina.Service");

    // 省略一些代码
    long t2=System.currentTimeMillis();
    if (log.isDebugEnabled()) {
        log.debug("Digester for server.xml created " + ( t2-t1 ));
    }
    return (digester);
}

Server、Server/Listener和Server/Service等看着很眼熟,很像server.xml中的元素。从Digester类看看server.xml是如何被解析的。

1. Digester类

Digester类继承了SAX包的DefaultHandler2,重写了如startElement,endElement等解析事件处理方法。其在内部维护了一个对象栈,用来保存解析过程中创建的对象。在Catalina的load方法中,digester.push(this);使Catalina实例自身在开始解析前先入栈。
createStartDigester方法调用了很多次addObjectCreate、addSetNext、addSetProperties和addRule方法,这些方法代码如下,它们都与Rule的概念有关。

public void addObjectCreate(String pattern, String className, String attributeName) {
    addRule(pattern, new ObjectCreateRule(className, attributeName));
}

public void addSetNext(String pattern, String methodName, String paramType) {
    addRule(pattern, new SetNextRule(methodName, paramType));
}

public void addSetProperties(String pattern) {
    addRule(pattern, new SetPropertiesRule());
}

2. Rule

简单地说,Rule就是解析XML时遇到符合某种模式的元素时执行的动作。抽象类Rule的代码如下所示,最重要的方法分别是begin、body和end,分别在遇到匹配元素的起始处、元素体(body)和结尾处触发。

public abstract class Rule {
    // 省略一些代码
    public void begin(String namespace, String name, Attributes attributes) throws Exception {
        // NO-OP by default.
    }

    public void body(String namespace, String name, String text) throws Exception {
        // NO-OP by default.
    }

    public void end(String namespace, String name) throws Exception {
        // NO-OP by default.
    }
}

从Digester的方法也可以看到Rule的各个方法的执行时机,startElement和endElement分别重写了DefaultHandler2的方法,表示在元素起始和结束时触发。从下述代码中可以看到startElement方法会执行匹配Rule的begin方法,而endElement方法会执行匹配Rule的body和end方法。

@Override
public void startElement(String namespaceURI, String localName, String qName, Attributes list)
        throws SAXException {
    // 省略一些代码
    // Fire "begin" events for all relevant rules
    List<Rule> rules = getRules().match(namespaceURI, match);
    matches.push(rules);
    if ((rules != null) && (rules.size() > 0)) {
        for (int i = 0; i < rules.size(); i++) {
            try {
                Rule rule = rules.get(i);
                if (debug) {
                    log.debug("  Fire begin() for " + rule);
                }
                rule.begin(namespaceURI, name, list);
            } catch (Exception e) {
                // 省略一些代码
            }
        }
    }
}

@Override
public void endElement(String namespaceURI, String localName, String qName)
        throws SAXException {
    // 省略一些代码
    // Fire "body" events for all relevant rules
    List<Rule> rules = matches.pop();
    if ((rules != null) && (rules.size() > 0)) {
        String bodyText = this.bodyText.toString();
        for (int i = 0; i < rules.size(); i++) {
            try {
                Rule rule = rules.get(i);
                if (debug) {
                    log.debug("  Fire body() for " + rule);
                }
                rule.body(namespaceURI, name, bodyText);
            } catch (Exception e) {
                // 省略一些代码
            }
        }
    }
    // Recover the body text from the surrounding element
    bodyText = bodyTexts.pop();
    // Fire "end" events for all relevant rules in reverse order
    if (rules != null) {
        for (int i = 0; i < rules.size(); i++) {
            int j = (rules.size() - i) - 1;
            try {
                Rule rule = rules.get(j);
                if (debug) {
                    log.debug("  Fire end() for " + rule);
                }
                rule.end(namespaceURI, name);
            } catch (Exception e) {
                // 省略一些代码
            }
        }
    }
    // 省略一些代码
}
  • 2.1 ObjectCreateRule类
    ObjectCreateRule类的代码如下,正如其类名暗示的那样,其在begin方法中会根据类名利用反射实例化对象,并将该对象压进Digester的对象栈,而end方法会使Digester对象栈做出栈操作。
    public class ObjectCreateRule extends Rule {
        // 省略一些代码
        @Override
        public void begin(String namespace, String name, Attributes attributes)
                throws Exception {
    
            // Identify the name of the class to instantiate
            String realClassName = className;
            if (attributeName != null) {
                String value = attributes.getValue(attributeName);
                if (value != null) {
                    realClassName = value;
                }
            }
            if (digester.log.isDebugEnabled()) {
                digester.log.debug("[ObjectCreateRule]{" + digester.match +
                        "}New " + realClassName);
            }
    
            if (realClassName == null) {
                throw new NullPointerException("No class name specified for " +
                        namespace + " " + name);
            }
    
            // Instantiate the new object and push it on the context stack
            Class<?> clazz = digester.getClassLoader().loadClass(realClassName);
            Object instance = clazz.getConstructor().newInstance();
            digester.push(instance);
        }
    }
    
    @Override
    public void end(String namespace, String name) throws Exception {
    
        Object top = digester.pop();
        if (digester.log.isDebugEnabled()) {
            digester.log.debug("[ObjectCreateRule]{" + digester.match +
                    "} Pop " + top.getClass().getName());
        }
    }
    
  • 2.2 SetPropertiesRule类
    SetPropertiesRule类的代码如下,正如其类名暗示的那样,其在begin方法中使用IntrospectionUtils类的setProperty方法调用ObjectCreateRule所创建实例的setter方法设置属性:
    public class SetPropertiesRule extends Rule {
        // 省略一些代码
        @Override
        public void begin(String namespace, String theName, Attributes attributes)
                throws Exception {
    
            // Populate the corresponding properties of the top object
            Object top = digester.peek();
            if (digester.log.isDebugEnabled()) {
                if (top != null) {
                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
                                    "} Set " + top.getClass().getName() +
                                    " properties");
                } else {
                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
                                    "} Set NULL properties");
                }
            }
    
            for (int i = 0; i < attributes.getLength(); i++) {
                String name = attributes.getLocalName(i);
                if ("".equals(name)) {
                    name = attributes.getQName(i);
                }
                String value = attributes.getValue(i);
    
                if (digester.log.isDebugEnabled()) {
                    digester.log.debug("[SetPropertiesRule]{" + digester.match +
                            "} Setting property '" + name + "' to '" +
                            value + "'");
                }
                if (!digester.isFakeAttribute(top, name)
                        && !IntrospectionUtils.setProperty(top, name, value)
                        && digester.getRulesValidation()) {
                    digester.log.warn("[SetPropertiesRule]{" + digester.match +
                            "} Setting property '" + name + "' to '" +
                            value + "' did not find a matching property.");
                }
            }
        }
    }
    
  • 2.3 SetNextRule类
    SetNextRule类的代码如下,其在end方法中使用IntrospectionUtils类的callMethod1方法在Digester对象栈栈顶元素和次栈顶元素间建立关联关系,即在次栈顶的对象上调用方法,参数是栈顶的对象。
    public class SetNextRule extends Rule {
        // 省略一些代码
        @Override
        public void end(String namespace, String name) throws Exception {
            // Identify the objects to be used
            Object child = digester.peek(0);
            Object parent = digester.peek(1);
            if (digester.log.isDebugEnabled()) {
                if (parent == null) {
                    digester.log.debug("[SetNextRule]{" + digester.match +
                            "} Call [NULL PARENT]." +
                            methodName + "(" + child + ")");
                } else {
                    digester.log.debug("[SetNextRule]{" + digester.match +
                            "} Call " + parent.getClass().getName() + "." +
                            methodName + "(" + child + ")");
                }
            }
    
            // Call the specified method
            IntrospectionUtils.callMethod1(parent, methodName,
                    child, paramType, digester.getClassLoader());
        }
    }
    
    以createStartDigester方法中的两行代码为例:
    • Server/Listener表示在处理<Server>元素的<Listener>子元素后需要执行SetNextRule规则,此时栈顶应该是Listener对象,次栈顶是Server对象。SetNextRule会调用Server的addLifecycleListener方法,参数即是Listener对象,参数类型是org.apache.catalina.LifecycleListener;
    • Server/Service/Listener表示在处理<Service>元素的<Listener>子元素后需要执行SetNextRule规则,此时栈顶应该是Listener对象,次栈顶是Service对象,再次栈顶才是Server对象。SetNextRule会调用Service的addLifecycleListener方法,参数即是Listener对象,参数类型是org.apache.catalina.LifecycleListener。
    digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
    digester.addSetNext("Server/Service/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
    

3. 解析实例

下面以Tomcat自带的server.xml为例说明这些Rule的作用,server.xml的内容如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
  <GlobalNamingResources>
    <Resource name="UserDatabase" auth="Container"
              type="org.apache.catalina.UserDatabase"
              description="User database that can be updated and saved"
              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
              pathname="conf/tomcat-users.xml" />
  </GlobalNamingResources>
  <Service name="Catalina">
    <Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
    <Engine name="Catalina" defaultHost="localhost">
      <Realm className="org.apache.catalina.realm.LockOutRealm">
        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
               resourceName="UserDatabase"/>
      </Realm>

      <Host name="localhost"  appBase="webapps"
            unpackWARs="true" autoDeploy="true">
        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
               prefix="localhost_access_log" suffix=".txt"
               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
    </Engine>
  </Service>
</Server>

以Server模式为例,createStartDigester方法为其添加了三个Rule:

digester.addObjectCreate("Server",  "org.apache.catalina.core.StandardServer", "className");
digester.addSetProperties("Server");
digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");

当XML解析器遇到server.xml里面的<Server>元素时:

  • 遇到<Server port="8005" shutdown="SHUTDOWN">时,首先ObjectCreateRule创建一个org.apache.catalina.core.StandardServer类型的实例,然后SetPropertiesRule将<Server>元素的属性和值设置到生成的实例上,即在StandardServer实例上调用setPort将port设置为8005、调用setShutdown将shutdown设置为SHUTDOWN;
  • 遇到<Server/>时,Digester对象栈栈顶是生成的StandardServer实例,次栈顶对象是Catalina实例(上文提到开始解析前Catalina实例自身先入栈),SetNextRule在Catalina实例上调用setServer方法,参数是栈顶的StandardServer实例,方法参数类型是org.apache.catalina.Server。

4. 解析结果

对照createStartDigester方法和Tomcat自带的server.xml,XML解析后生成了如下组件:

  • 4.1 Server元素
    digester.addObjectCreate("Server", "org.apache.catalina.core.StandardServer", "className");
    digester.addSetProperties("Server");
    digester.addSetNext("Server", "setServer", "org.apache.catalina.Server");
    

    创建了一个StandardServer实例,关闭端口是8005,关闭命令是SHUTDOWN。

  • 4.2 Listener元素
    digester.addObjectCreate("Server/Listener",
                             null, // MUST be specified in the element
                             "className");
    digester.addSetProperties("Server/Listener");
    digester.addSetNext("Server/Listener", "addLifecycleListener", "org.apache.catalina.LifecycleListener");
    

    分别创建了VersionLoggerListener、AprLifecycleListener、JreMemoryLeakPreventionListener、GlobalResourcesLifecycleListener和ThreadLocalLeakPreventionListener实例,这些Listener都被添加到了StandardServer上。

  • 4.3 Service元素
    digester.addObjectCreate("Server/Service", "org.apache.catalina.core.StandardService", "className");
    digester.addSetProperties("Server/Service");
    digester.addSetNext("Server/Service", "addService", "org.apache.catalina.Service");
    

    创建StandardService实例,这个实例被添加到了SnadardServer上。

  • 4.4 Connector元素
    digester.addRule("Server/Service/Connector", new ConnectorCreateRule());
    digester.addRule("Server/Service/Connector", new SetAllPropertiesRule(new String[]{"executor", "sslImplementationName"}));
    digester.addSetNext("Server/Service/Connector", "addConnector",  "org.apache.catalina.connector.Connector");
    

    ConnectorCreateRule的部分源码如下:

    @Override
    public void begin(String namespace, String name, Attributes attributes)
            throws Exception {
        Service svc = (Service)digester.peek();
        Executor ex = null;
        if ( attributes.getValue("executor")!=null ) {
            ex = svc.getExecutor(attributes.getValue("executor"));
        }
        Connector con = new Connector(attributes.getValue("protocol"));
        if (ex != null) {
            setExecutor(con, ex);
        }
        String sslImplementationName = attributes.getValue("sslImplementationName");
        if (sslImplementationName != null) {
            setSSLImplementationName(con, sslImplementationName);
        }
        digester.push(con);
    }
    

    ConnectorCreateRule的begin方法中调用了Connector类的构造方法,参数是xml属性protocol的值,相关代码如下:

    public Connector(String protocol) {
        setProtocol(protocol);
        // Instantiate protocol handler
        ProtocolHandler p = null;
        try {
            Class<?> clazz = Class.forName(protocolHandlerClassName);
            p = (ProtocolHandler) clazz.getConstructor().newInstance();
        } catch (Exception e) {
            log.error(sm.getString(
                    "coyoteConnector.protocolHandlerInstantiationFailed"), e);
        } finally {
            this.protocolHandler = p;
        }
    
        if (Globals.STRICT_SERVLET_COMPLIANCE) {
            uriCharset = StandardCharsets.ISO_8859_1;
        } else {
            uriCharset = StandardCharsets.UTF_8;
        }
    }
    
    public void setProtocol(String protocol) {
        boolean aprConnector = AprLifecycleListener.isAprAvailable() &&
                AprLifecycleListener.getUseAprConnector();
        if ("HTTP/1.1".equals(protocol) || protocol == null) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11AprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.http11.Http11NioProtocol");
            }
        } else if ("AJP/1.3".equals(protocol)) {
            if (aprConnector) {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpAprProtocol");
            } else {
                setProtocolHandlerClassName("org.apache.coyote.ajp.AjpNioProtocol");
            }
        } else {
            setProtocolHandlerClassName(protocol);
        }
    }
    
    public void setProtocolHandlerClassName(String protocolHandlerClassName) {
        this.protocolHandlerClassName = protocolHandlerClassName;
    }
    
    • setProtocol方法根据xml属性protocol的值设置成员变量protocolHandlerClassName的值;aprConnector变量的含义是是否使用Tomcat的APR库替代Java NIO,默认是false,如何启用请参阅Connector文档的protocol属性一栏;
    • Connector构造方法接着根据protocolHandlerClassName利用反射实例化ProtocolHandler;

    server.xml中的HTTP/1.1和AJP/1.3分别创建了用Http11NioProtocol处理的Connector和用AjpNioProtocol处理的Connector两个实例。

  • 4.5 其他

    其他元素的解析过程同理,在此不再赘述,有兴趣的读者可自行阅读完整的createStartDigester方法。

为Server实例设置目录属性

getServer方法返回XML解析过程中生成的Server实例,默认实现类是StandardServer,以下代码为该实例设置目录属性。

getServer().setCatalina(this);
getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

initStreams方法

initStreams方法调用System类的方法重定向标准输出和标准错误输出:

protected void initStreams() {
    // Replace System.out and System.err with a custom PrintStream
    System.setOut(new SystemLogHandler(System.out));
    System.setErr(new SystemLogHandler(System.err));
}

Server实例初始化

Server实例初始化和后面Catalina类的start方法主要是调用StandardServer实例的init和start方法,它们都是org.apache.catalina.Lifecycle接口的方法,具体分析请参见后续文章。

参考文献

Apache Tomcat 8 Architecture
Apache Tomcat 8 Startup

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

推荐阅读更多精彩内容