Tomcat的生命周期(三)

前言
在上一篇文章Tomcat的生命周期(二)中我们分析了Container所有子容器的初始化和启动过程、Connector连接器的初始化,并介绍了Tomcat实现热加载的原理,本文同样基于之前所有Tomcat系列文章,主要对Connector的启动过程进行讲解,同时介绍MapperMapperListener的运行机制,为下一篇讲解Tomcat对请求响应的处理做铺垫

Connector的启动入口在StandardServicestartInternal()中,最终调用Connector.startInternal()

图1. Connector的startInternal()

启动方法可以分成两部分:1. 启动协议处理类,对于本文来说Http11Protocol是其具体实现类,初始化过程已在Tomcat架构中各个组件及组件间关系(二)中分析过;2. 容器组件映射关系监听器MapperListener启动,该类非常重要,保存了HostContextWrapper之间的映射关系,试想一下,当一个请求过来时Tomcat是如何知道请求对应的是哪个war包,哪个Servlet呢?MapperListenerMapper类就做了请求“引路人”的作用。我们先看第一部分
本文的分析可能设置各种内部类、方法之间的跳转,读者可以借助Tomcat的生命周期(二)中图6提供的类图帮助理解。protocolHandler.start()会调用协议处理父类AbstractProtocol.start()
图2. AbstractProtocol的start()

方法内调用了端到端处理类JIoEndpoint.start(),实际会调用父类AbstractEndpoint.start()
图3. AbstractEndpoint的start()

模板方法进入具体实现JIoEndpoint.startInternal(),如果读者顺着Tomcat系列文章顺序看下来,应该对这个“套路”非常熟悉了,我们就不把时间浪费在重复了很多次的思路上面了
图4. JIoEndpoint的startInternal()

Tomcat架构中各个组件及组件间关系(二)中讲到解析server.xml<Connector>时曾经说过,默认情况下Connector是没有线程池的,但是即使不在server.xml中设置executor在启动Connector时Tomcat也会创建一个默认的线程池,对应的就是这里的createExecutor(),从严谨的角度来说,这个线程池适用于处理端到端连接的线程池,即属于AbstractEndpoint及其子类
图5. AbstractEndpoint的createExecutor()

TaskQueue继承自LinkedBlockingQueue并重写了关键的take()offer(Runnable)方法,通过创建的线程工厂TaskThreadFactory设置了线程池的名称,开启守护线程并设置优先级为NORMAL。线程池构造器中传递的参数分别设置corePoolSize = 10maxPoolSize = 200keepAliveTime = 60s
图4中的InitializeConnectionLatch()设置了端到端处理类最大连接数量为200,该数字在JIoEndpoint.bind()中进行了设置,最后创建了一个异步请求超时线程,不是我们讲解的重点,我们来看下startAcceptorThreads()
图6. AbstractEndpoint的startAcceptorThreads()

第一句得到Acceptor线程的数量,该值同样在初始化时由JIoEndpoint.bind()中进行了设置为1,调用createAcceptor()创建对应端到端类型的Acceptor线程,对应代码清单1

protected class Acceptor extends AbstractEndpoint.Acceptor {

        @Override
        public void run() {

            int errorDelay = 0;

            // Loop until we receive a shutdown command
            while (running) {

                // Loop if endpoint is paused
                while (paused && running) {
                    state = AcceptorState.PAUSED;
                    try {
                        Thread.sleep(50);
                    } catch (InterruptedException e) {
                        // Ignore
                    }
                }

                if (!running) {
                    break;
                }
                state = AcceptorState.RUNNING;

                try {
                    //if we have reached max connections, wait
                    //    (1)
                    countUpOrAwaitConnection();

                    Socket socket = null;
                    try {

                        // Accept the next incoming connection from the server
                        // socket
                        //    (2)
                        socket = serverSocketFactory.acceptSocket(serverSocket);
                    } catch (IOException ioe) {
                      //    (3)
                        countDownConnection();
                        // Introduce delay if necessary
                        errorDelay = handleExceptionWithDelay(errorDelay);
                        // re-throw
                        throw ioe;
                    }
                    // Successful accept, reset the error delay
                    errorDelay = 0;

                    // Configure the socket
                    //    (4)
                    if (running && !paused && setSocketOptions(socket)) {

                        // Hand this socket off to an appropriate processor
                        //    (5)
                        if (!processSocket(socket)) {

                            countDownConnection();

                            // Close socket right away
                            closeSocket(socket);
                        }
                    } else {
                        countDownConnection();
                        // Close socket right away
                        closeSocket(socket);
                    }
                } catch (IOException x) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), x);
                    }
                } catch (NullPointerException npe) {
                    if (running) {
                        log.error(sm.getString("endpoint.accept.fail"), npe);
                    }
                } catch (Throwable t) {
                    ExceptionUtils.handleThrowable(t);
                    log.error(sm.getString("endpoint.accept.fail"), t);
                }
            }
            state = AcceptorState.ENDED;
        }
    }

标注(1)对当前连接数进行判断,如果超过了阈值200则阻塞等待其他连接释放,底层用了AQS的无阻塞锁机制。之前分析过默认情况下或者说没有开启SSL的情况下产生Socket的工厂为DefaultServerSocketFactory,标注(2)底层其实就是服务端阻塞等待socket连接的过程,当连接过程出现异常时由标注(3)的代码释放latch门栓,防止资源被白白占用。标注(4)设置了一些socket的连接参数,Tomcat中将所有socket参数封装在SocketProperties中,在使用过程中我们可以根据请求状况调整这些参数。比如,在Tomcat的生命周期(二)中初始化协议处理类Http11Protocol时设置了socket连接超时时间,是否支持延迟等参数。标注(5)是处理请求的入口

图7. JIoEndpoint的processSocket()

SocketProcessor是在每一种类型的端到端处理类中的内部类,实现了Runnable,总的来说一个Acceptor线程监听端口得到socket,一个socket又对应一个SocketProcessor线程,而所有的SocketProcessor又会在一个corePoolSize = 10maxPoolSize = 200的线程池中运行,进入SocketProcessor意味着正式进入Tomcat处理请求响应的流程中,将在下一篇文章中详细分析
接着我们分析一下图1中启动的第二部分,在正式开始之前,我们先找到mapperListener是何时创建的,因为之前的文章中并没有说到该类。实际上该类和另外一个有关系的类MapperConnector的两个成员变量,在创建Connector时一同创建
图8. Mapper及MapperListener

Mapper中保存了所有Container容器的对应关系,类中有几个内部类MapElementHostContextListContextContextVersionWrapper,其中HostContextWrapper继承了抽象类MapElement,其中包含两个元素:1. name表示对应Container容器的名称;2. object表示容器本身对象。Host中持有ContextList的引用,并维护了一个保存该Host所有alias的集合;ContextList持有Context[]的引用;Context中维护了一个ContextVersion[]保存了一个war包的不同版本实例;ContextVersion表示了某一特定版本的war包,其下必有代表多个ServletWrapper数组
MapperListener实现了两个监听器接口,一个是经常出镜的LifecycleListener,针对Tomcat整体生命周期进行监听;另一个是只用来监听Container相关事件的ContainerListener。前一个已经分析吐了,这里不再累述。所有Container特有事件都保存在Container接口中
图9. Container相关事件

当有上述任意事件发生时,Container容器会首先调用ContainerBase.fireContainerEvent(String, Object),进而封装成ContainerEvent,调用ContainerListener.conatinerEvent(ContainerEvent)的具体实现,由感兴趣的监听器进行处理,至于MapperListenerconatinerEvent(ContainerEvent)的分析暂且放一放,先回到主题MapperListenerstartInternal()上来
图10. MapperListener的startInternal()

findDefaultHost()设置默认的Host
图11. MapperListener的findDefaultHost()

StandardService作为Tomcat两大组件的“组合器”,因此Connector需要通过上层容器StandardService做一次中转找到对应的Container容器StandardEngine,然后得到<Engine>中配置的defaultHost属性的值,再与StandardEngine下所有的StandardHost一一比较,如果存在对应的实体(存在name属性与<Engine> defaultHost属性相同的<Host>标签),就可以设置默认host名称为<Engine>defaultHost属性的值
图12. MapperListener的addListeners(Container)

图10中addListeners(engin)如上图所示,采用了和ContainerBackgroundProcessor.processChildren(Container, ClassLoader)同样的递归处理,让StandardEngine下所有的children都添加了MapperListener。最后看一下registerHost(Host)
图13. MapperListener的registerHost(Host)

代码中得到待注册Host所有的别名,将别名数组,Host名称和对象本身塞入addHost(String, String[], Object)
图14. Mapper的addHost(String, String[], Object)

addService(Service)等添加子容器的方法思路一样,这里添加一个Host也首先创建一个比原数组大1的新数组,然后通过insertMap(Mapper.MapElement[], Mapper.MapElment[], Mapper.MapElement)方法将老数组copy到新数组中,最后将老数组的引用指向新数组
图15. Mapper的insertMap(Mapper.MapElement[], Mapper.MapElment[], Mapper.MapElement)

该方法是一个公共抽取方法,所有继承MapElement的映射组件都能通过该方法完成添加操作。其中find(MapElement[], String)根据第二个参数(新元素的名称,不限于Host的名称)与第一个参数的数组中元素的名称进行比较(数组中元素根据名称有序排列),返回名称相同元素或者closest inferior元素(知道意思但不会用中文如何优雅的表达,抱歉,哈哈)的索引,该索引就是新元素要插入的索引减一,如果找到同名的元素,该方法会返回false,新重复元素在图14else中代码会覆盖老重复元素。最后会将新Host元素的所有alias与该元素进行关联
我们回到图13注册Host流程的后半部分,在Mapper添加新Host之后会遍历该Host下所有的children并开始registerContext(Context)
图16. MapperListener的registerContext(Context)

从上图中可以发现在将Context真正放入Mapper之前程序首先遍历了Context下所有的StandardWrapper,并调用prepareWrapperMappingInfo(Context, Wrapper, List<WrapperMappingInfo>)
图17. MapperListener的prepareWrapperMappingInfo(Context, Wrapper, List<WrapperMappingInfo>)

Tomcat架构中各个组件及组件间关系(二)中我们曾经分析过解析web.xml的规则,文件中就包含对于<servlet-mapping>标签的处理,当解析到该标签时会调用WebXmladdServletMapping(String, String),方法的两个参数对应了<servlet-mapping>两个子标签<url-pattern><servlet-name>的值,所有的<servlet-mapping>标签解析后都会放在Map<String,String> servletMappings集合中。而在StandardContext启动的流程中会发送CONFIGURE_START_EVENTContextConfig,进而产生configureStart()-->webConfig()-->configureContext(Context)-->StandardContext.addServletMapping(String, String)最终将WebXmlserlvetMappings的values(所有<servlet-mapping><url-pattern>的集合)放入StandardWrapper的成员变量ArrayList<String> mappings中,该变量就对应上图中的mappings数组。之后遍历所有的<servlet-mapping>映射,如果servlet name为jsp并且<url-pattern>以通配符/*结束,则认为该Servlet是专门处理jsp的Servlet,置标志位jspWildCard为true。最后将封装好的WrapperMappingInfo放入参数集合wrappers
回到图16,最后将HostContextWrapperMappingInfo集合等信息传入addContextVersion方法中,虽然我能理解这个方法为什么要传递这么多参数,可能是因为添加的ContextVersion对象属于承上启下的中间对象,既作为Context中的一个版本对象,也要处理下属的Wrapper对象间关系,但是还是觉得传递这么多参数对于一个方法而言略多,我们在代码清单2中分析一下该方法

/**
 * Add a new Context to an existing Host.
 *
 * @param hostName Virtual host name this context belongs to
 * @param host Host object
 * @param path Context path
 * @param version Context version
 * @param context Context object
 * @param welcomeResources Welcome files defined for this context
 * @param resources Static resources of the context
 * @param wrappers Information on wrapper mappings
 * @param mapperContextRootRedirectEnabled Mapper does context root redirects
 * @param mapperDirectoryRedirectEnabled Mapper does directory redirects
 */
public void addContextVersion(String hostName, Object host, String path,
        String version, Object context, String[] welcomeResources,
        javax.naming.Context resources, Collection<WrapperMappingInfo> wrappers,
        boolean mapperContextRootRedirectEnabled, boolean mapperDirectoryRedirectEnabled) {
    //    (1)
    Host mappedHost = exactFind(hosts, hostName);
    if (mappedHost == null) {
        addHost(hostName, new String[0], host);
        mappedHost = exactFind(hosts, hostName);
        if (mappedHost == null) {
            log.error("No host found: " + hostName);
            return;
        }
    }
    //    (2)
    if (mappedHost.isAlias()) {
        log.error("No host found: " + hostName);
        return;
    }
    int slashCount = slashCount(path);
    synchronized (mappedHost) {
      //    (3)
        ContextVersion newContextVersion = new ContextVersion(version, context);
        newContextVersion.path = path;
        newContextVersion.slashCount = slashCount;
        newContextVersion.welcomeResources = welcomeResources;
        newContextVersion.resources = resources;
        newContextVersion.mapperContextRootRedirectEnabled = mapperContextRootRedirectEnabled;
        newContextVersion.mapperDirectoryRedirectEnabled = mapperDirectoryRedirectEnabled;

        if (wrappers != null) {
            //    (4)
            addWrappers(newContextVersion, wrappers);
        }

        ContextList contextList = mappedHost.contextList;
        //    (5)
        Context mappedContext = exactFind(contextList.contexts, path);
        if (mappedContext == null) {
            mappedContext = new Context(path, newContextVersion);
            //    (6)
            ContextList newContextList = contextList.addContext(
                    mappedContext, slashCount);
            if (newContextList != null) {
                //    (7)
                updateContextList(mappedHost, newContextList);
            }
        } else {
            ContextVersion[] contextVersions = mappedContext.versions;
            ContextVersion[] newContextVersions =
                new ContextVersion[contextVersions.length + 1];
            if (insertMap(contextVersions, newContextVersions, newContextVersion)) {
                //    (8)
                mappedContext.versions = newContextVersions;
            } else {
                // Re-registration after Context.reload()
                // Replace ContextVersion with the new one
                //    (9)
                int pos = find(contextVersions, version);
                if (pos >= 0 && contextVersions[pos].name.equals(version)) {
                    contextVersions[pos] = newContextVersion;
                }
            }
        }
    }

}

方法每个参数的含义在注释中写的很清楚,我们主要看代码逻辑。标注(1)从Host[]中查找匹配第二个参数hostNameHost,如果没有找到,说明该Host还没有注册,调用addHost(String, String[], Object)先添加到映射中,之后进行二次校验判断是否添加成功,如果还没有添加成功则结束流程。标注(2)说明了一点,Host必须存在别名,否则无法执行操作。标注(3)根据参数构建出本次版本的ContextVersion,如果参数wrappers不为空,则先进行Wrapper的映射添加,addWrappers(ContextVersion, Collection<WrapperMappingInfo>)最终会调用代码清单3 中展示的方法

/**
 * Adds a wrapper to the given context.
 *
 * @param context The context to which to add the wrapper
 * @param path Wrapper mapping
 * @param wrapper The Wrapper object
 * @param jspWildCard true if the wrapper corresponds to the JspServlet
 *   and the mapping path contains a wildcard; false otherwise
 * @param resourceOnly true if this wrapper always expects a physical
 *                     resource to be present (such as a JSP)
 */
protected void addWrapper(ContextVersion context, String path,
        Object wrapper, boolean jspWildCard, boolean resourceOnly) {

    synchronized (context) {
        if (path.endsWith("/*")) {
            // Wildcard wrapper
            String name = path.substring(0, path.length() - 2);
            Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                    resourceOnly);
            Wrapper[] oldWrappers = context.wildcardWrappers;
            Wrapper[] newWrappers =
                new Wrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.wildcardWrappers = newWrappers;
                int slashCount = slashCount(newWrapper.name);
                if (slashCount > context.nesting) {
                    context.nesting = slashCount;
                }
            }
        } else if (path.startsWith("*.")) {
            // Extension wrapper
            String name = path.substring(2);
            Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                    resourceOnly);
            Wrapper[] oldWrappers = context.extensionWrappers;
            Wrapper[] newWrappers =
                new Wrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.extensionWrappers = newWrappers;
            }
        } else if (path.equals("/")) {
            // Default wrapper
            Wrapper newWrapper = new Wrapper("", wrapper, jspWildCard,
                    resourceOnly);
            context.defaultWrapper = newWrapper;
        } else {
            // Exact wrapper
            final String name;
            if (path.length() == 0) {
                // Special case for the Context Root mapping which is
                // treated as an exact match
                name = "/";
            } else {
                name = path;
            }
            Wrapper newWrapper = new Wrapper(name, wrapper, jspWildCard,
                    resourceOnly);
            Wrapper[] oldWrappers = context.exactWrappers;
            Wrapper[] newWrappers =
                new Wrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.exactWrappers = newWrappers;
            }
        }
    }
}

从代码中可以和明显的看出,根据path参数(对应<url-pattern>)的不同,逻辑分为四个部分:1. 以/*结尾的通配符匹配规则;2. 以*.开始的扩展名匹配规则;3. 代表默认匹配规则的路径/;4. 不满足上述三种的精确名匹配规则。如果大家对Servlet有一定深度了解的话就会秒懂,这里的四种路径匹配分类正好对应了Servlet的四种匹配规则,而这四种配置的Wrapper会分别放置在ContextVersion中对应的Wrapper[]

图18. ContextVersion中四种不同匹配路径对应的四种Wrapper

我们回到代码清单2,标注(5)根据context pathContext[]中寻找匹配项,如果不存在匹配context pathContext,进入新增Context流程,ContextList.addContext(Context, int)将新增的Context放入ContextList中,而updateContextList(Host, ContextList)更新改动后ContextList所属Host内的引用;如果存在同路径Context则进入添加同路径Context不同版本ContextVersion流程,调用inserMap(MapElement[], MapElement[], MapElement)进行顺位插入,如果发现存在一个同版本的ContextVersion对象,则插入失败,进入最后的else流程,找到重复version的ContextVersion并用新元素覆盖老元素
至此所有元素地址对应元素实体的关系都存储在Mapper中,当请求到来时,可以根据StandardHost中的成员变量mapper定位到具体的Servlet,最后我们再来看看上面提到的ContainerEvent触发方法,代码清单4

@Override
public void containerEvent(ContainerEvent event) {

    if (Container.ADD_CHILD_EVENT.equals(event.getType())) {
        Container child = (Container) event.getData();
        addListeners(child);
        // If child is started then it is too late for life-cycle listener
        // to register the child so register it here
        if (child.getState().isAvailable()) {
            if (child instanceof Host) {
                registerHost((Host) child);
            } else if (child instanceof Context) {
                registerContext((Context) child);
            } else if (child instanceof Wrapper) {
                // Only if the Context has started. If it has not, then it
                // will have its own "after_start" life-cycle event later.
                if (child.getParent().getState().isAvailable()) {
                    registerWrapper((Wrapper) child);
                }
            }
        }
    } else if (Container.REMOVE_CHILD_EVENT.equals(event.getType())) {
        Container child = (Container) event.getData();
        removeListeners(child);
        // No need to unregister - life-cycle listener will handle this when
        // the child stops
    } else if (Host.ADD_ALIAS_EVENT.equals(event.getType())) {
        // Handle dynamically adding host aliases
        mapper.addHostAlias(((Host) event.getSource()).getName(),
                event.getData().toString());
    } else if (Host.REMOVE_ALIAS_EVENT.equals(event.getType())) {
        // Handle dynamically removing host aliases
        mapper.removeHostAlias(event.getData().toString());
    } else if (Wrapper.ADD_MAPPING_EVENT.equals(event.getType())) {
        // Handle dynamically adding wrappers
        Wrapper wrapper = (Wrapper) event.getSource();
        Context context = (Context) wrapper.getParent();
        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }
        String version = context.getWebappVersion();
        String hostName = context.getParent().getName();
        String wrapperName = wrapper.getName();
        String mapping = (String) event.getData();
        boolean jspWildCard = ("jsp".equals(wrapperName)
                && mapping.endsWith("/*"));
        mapper.addWrapper(hostName, contextPath, version, mapping, wrapper,
                jspWildCard, context.isResourceOnlyServlet(wrapperName));
    } else if (Wrapper.REMOVE_MAPPING_EVENT.equals(event.getType())) {
        // Handle dynamically removing wrappers
        Wrapper wrapper = (Wrapper) event.getSource();

        Context context = (Context) wrapper.getParent();
        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }
        String version = context.getWebappVersion();
        String hostName = context.getParent().getName();

        String mapping = (String) event.getData();

        mapper.removeWrapper(hostName, contextPath, version, mapping);
    } else if (Context.ADD_WELCOME_FILE_EVENT.equals(event.getType())) {
        // Handle dynamically adding welcome files
        Context context = (Context) event.getSource();

        String hostName = context.getParent().getName();

        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }

        String welcomeFile = (String) event.getData();

        mapper.addWelcomeFile(hostName, contextPath,
                context.getWebappVersion(), welcomeFile);
    } else if (Context.REMOVE_WELCOME_FILE_EVENT.equals(event.getType())) {
        // Handle dynamically removing welcome files
        Context context = (Context) event.getSource();

        String hostName = context.getParent().getName();

        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }

        String welcomeFile = (String) event.getData();

        mapper.removeWelcomeFile(hostName, contextPath,
                context.getWebappVersion(), welcomeFile);
    } else if (Context.CLEAR_WELCOME_FILES_EVENT.equals(event.getType())) {
        // Handle dynamically clearing welcome files
        Context context = (Context) event.getSource();

        String hostName = context.getParent().getName();

        String contextPath = context.getPath();
        if ("/".equals(contextPath)) {
            contextPath = "";
        }

        mapper.clearWelcomeFiles(hostName, contextPath,
                context.getWebappVersion());
    }
}

事件处理中涉及的核心逻辑和方法本文中都细细分析过了,比如事件ADD_CHILD_EVENT流程中首先会调用addListener(Container)用递归方式将新添加的Container下所有children都加上MapperListener,再根据添加容器的不同类型调用不同的register方法。另外ADD_MAPPING_EVENTREMOVE_MAPPING_EVENT事件只是添加/删除Wrapper的映射

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

推荐阅读更多精彩内容

  • 概述 Tomcat是一个JSP/Servlet容器。其作为Servlet容器,有三种工作模式:独立的Servlet...
    jiangmo阅读 2,224评论 0 13
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,184评论 11 349
  • 转自陈明乾的博客,可能有一定更新。 转原文声明: 原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 ...
    LUNJINGJIE阅读 3,964评论 1 33
  • WEB服务器 只要Web上的Server都叫Web Server,但是大家分工不同,解决的问题也不同,所以根据We...
    Rick617阅读 15,524评论 1 13
  • 今天已经是小李失业的第三个月了 每天只靠一包方便面的他 已经严重的营养不良 本来虚弱的他 更加变得额外虚弱 想想这...
    追风少年东北余文乐阅读 243评论 0 0