深入理解Tomcat(九)MapperListener和Mapper

前言

為了能夠快速地通過(guò)指定的uri找到對(duì)應(yīng)的wrapper及servlet,tomcat開(kāi)發(fā)人員設(shè)計(jì)出了兩個(gè)組件:MapperListenerMapper此蜈。

MapperListener主要作用如下:

  1. 通過(guò)監(jiān)聽(tīng)容器的AFTER_START_EVENT事件來(lái)對(duì)容器進(jìn)行注冊(cè)她奥;
  2. 通過(guò)監(jiān)聽(tīng)容器的BEFORE_STOP_EVENT事件來(lái)完成對(duì)容器的取消注冊(cè)讯泣。

而Mapper作為uri映射到容器的工具阵漏,扮演的角色就是一個(gè)映射組件预侯。它會(huì)緩存所有容器信息(包括容器名稱木羹、容器本身甲雅、容器層級(jí)等等),同時(shí)提供映射規(guī)則坑填,將一個(gè)uri按照映射規(guī)則映射到具體的Host抛人、Context和Wrapper,并最終通過(guò)Wrapper找到邏輯處理單元Servlet脐瑰。

本小節(jié)我們會(huì)對(duì)MapperListenerMapper緩存容器信息這一塊的源碼進(jìn)行分析妖枚,將映射邏輯放在第十小節(jié)。

那么苍在,讓我們開(kāi)始我們的征程吧~

入口

org.apache.catalina.core.StandardService類中绝页,我們看到兩個(gè)關(guān)鍵的字段--mappermapperListenter荠商。其中mapperListener依賴service對(duì)象進(jìn)行構(gòu)造。

/**
 * Mapper.
 */
protected final Mapper mapper = new Mapper();
/**
 * Mapper listener.
 */
protected final MapperListener mapperListener = new MapperListener(this);

1. MapperListener的注冊(cè)過(guò)程

MapperListener的構(gòu)造方法比較簡(jiǎn)單续誉,僅僅將mapper和service存儲(chǔ)到當(dāng)前對(duì)象的相關(guān)屬性中莱没。

public MapperListener(Service service) {
    this.service = service;
    this.mapper = service.getMapper();
}

MapperListener在構(gòu)造完成之后,會(huì)調(diào)用其start()方法酷鸦,我們來(lái)看看主要做了哪些事情饰躲?

  1. engine容器不存在,則MapperListener也不需要啟動(dòng)
  2. 查找默認(rèn)主機(jī)臼隔,并設(shè)置到mapper的defaultHostName屬性中
  3. 對(duì)容器及下面的所有子容器添加事件監(jiān)聽(tīng)器
  4. 注冊(cè)engine下面的host嘹裂、context和wrapper,registerHost會(huì)注冊(cè)host及下面的子容器
public void startInternal() throws LifecycleException {
    setState(LifecycleState.STARTING);

    // 1. engine容器不存在摔握,則MapperListener也不需要啟動(dòng)
    Engine engine = service.getContainer();
    if (engine == null) {
        return;
    }

    // 2. 查找默認(rèn)主機(jī)焦蘑,并設(shè)置到mapper的defaultHostName屬性中
    findDefaultHost();

    // 3. 對(duì)容器及下面的所有子容器添加事件監(jiān)聽(tīng)器
    addListeners(engine);

    // 4. 注冊(cè)engine下面的host、context和wrapper盒发,registerHost會(huì)注冊(cè)host及下面的子容器
    Container[] conHosts = engine.findChildren();
    for (Container conHost : conHosts) {
        Host host = (Host) conHost;
        if (!LifecycleState.NEW.equals(host.getState())) {
            // Registering the host will register the context and wrappers
            registerHost(host);
        }
    }
}

接著我們看看方法findDefaultHost例嘱,主要目的是檢查并設(shè)置mapperdefaultHostName屬性。

private void findDefaultHost() {
    // 獲取engine下面配置的defaultHost屬性
    Engine engine = service.getContainer();
    String defaultHost = engine.getDefaultHost();

    boolean found = false;

    // 如果defaultHost屬性不為空宁舰,則查找hosts下面的所有主機(jī)名及別名倔韭。
    // 1. 找到了則設(shè)置到mapper的defaultHostName屬性
    // 2. 沒(méi)找到則記錄警告信息
    if (defaultHost != null && defaultHost.length() >0) {
        Container[] containers = engine.findChildren();

        for (Container container : containers) {
            Host host = (Host) container;
            if (defaultHost.equalsIgnoreCase(host.getName())) {
                found = true;
                break;
            }

            String[] aliases = host.findAliases();
            for (String alias : aliases) {
                if (defaultHost.equalsIgnoreCase(alias)) {
                    found = true;
                    break;
                }
            }
        }
    }

    if(found) {
        mapper.setDefaultHostName(defaultHost);
    } else {
        log.warn(sm.getString("mapperListener.unknownDefaultHost",
                defaultHost, service));
    }
}

接下來(lái)我們分析addListeners疲迂,該方法用于對(duì)所有容器設(shè)置監(jiān)聽(tīng)器。它是一個(gè)遞歸方法!

private void addListeners(Container container) {
    // 對(duì)當(dāng)前容器添加容器監(jiān)聽(tīng)器和生命周期監(jiān)聽(tīng)器早处,也就是當(dāng)前對(duì)象
    container.addContainerListener(this);
    container.addLifecycleListener(this);
    // 對(duì)當(dāng)前容器下的子容器執(zhí)行addListeners操作
    for (Container child : container.findChildren()) {
        addListeners(child);
    }
}

接下來(lái)我們分析registerHost,該方法用于往mapper中注冊(cè)虛擬主機(jī)

/**
 * 注冊(cè)虛擬主機(jī)
 * Register host.
 */
private void registerHost(Host host) {

    String[] aliases = host.findAliases();
    // 往mapper中添加主機(jī)
    mapper.addHost(host.getName(), aliases, host);

    // 注冊(cè)host下的每個(gè)context
    for (Container container : host.findChildren()) {
        if (container.getState().isAvailable()) {
            registerContext((Context) container);
        }
    }
    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.registerHost",
                host.getName(), domain, service));
    }
}

registerHost除了注冊(cè)虛擬主機(jī)转绷,額外會(huì)調(diào)用registerContext來(lái)注冊(cè)context修然。我們看看這個(gè)方法。該方法完成了以下操作:

  1. contextPath如果為斜杠袜刷,則統(tǒng)一轉(zhuǎn)換為空字符串
  2. 將context下面的每個(gè)wrapper都添加到mapper
  3. 將context添加到mapper
/**
 * 注冊(cè)context
 * Register context.
 */
private void registerContext(Context context) {
    // contextPath如果為斜杠聪富,則統(tǒng)一轉(zhuǎn)換為空字符串
    String contextPath = context.getPath();
    if ("/".equals(contextPath)) {
        contextPath = "";
    }
    Host host = (Host)context.getParent();

    WebResourceRoot resources = context.getResources();
    String[] welcomeFiles = context.findWelcomeFiles();
    List<WrapperMappingInfo> wrappers = new ArrayList<>();

    // 將context下面的每個(gè)wrapper都添加到mapper
    for (Container container : context.findChildren()) {
        // 準(zhǔn)備wrapper信息,以便后續(xù)插入mapper
        prepareWrapperMappingInfo(context, (Wrapper) container, wrappers);

        if(log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.registerWrapper",
                    container.getName(), contextPath, service));
        }
    }

    // 將context添加到mapper
    mapper.addContextVersion(host.getName(), host, contextPath,
            context.getWebappVersion(), context, welcomeFiles, resources,
            wrappers);

    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.registerContext",
                contextPath, service));
    }
}

關(guān)鍵方法為prepareWrapperMappingInfo著蟹,用于準(zhǔn)備注冊(cè)到mapper下的wrapper墩蔓,這兒mapper對(duì)于wrapper的支持是wrapper的包裝對(duì)象--WrapperMappingInfo。而一個(gè)context可能有多個(gè)wrapper萧豆,所以WrapperMappingInfo是一個(gè)list奸披。我們來(lái)分析一下這個(gè)list對(duì)象的生成方法--prepareWrapperMappingInfo

該方法就是將映射url涮雷、wrapper名字資源只讀標(biāo)記等信息組合成對(duì)象添加到wrappers中阵面。

private void prepareWrapperMappingInfo(Context context, Wrapper wrapper,
        List<WrapperMappingInfo> wrappers) {
    String wrapperName = wrapper.getName();
    boolean resourceOnly = context.isResourceOnlyServlet(wrapperName);
    String[] mappings = wrapper.findMappings();
    for (String mapping : mappings) {
        boolean jspWildCard = (wrapperName.equals("jsp")
                               && mapping.endsWith("/*"));
        wrappers.add(new WrapperMappingInfo(mapping, wrapper, jspWildCard,
                resourceOnly));
    }
}

2. MapperListener的取消注冊(cè)過(guò)程

在tomcat組件中,start()的逆向過(guò)程為stop()MapperListener組件也不例外样刷。我們來(lái)分析一下其stop()方法仑扑,該方法的作用是將當(dāng)前監(jiān)聽(tīng)器從容器中移除。

@Override
public void stopInternal() throws LifecycleException {
    setState(LifecycleState.STOPPING);

    Engine engine = service.getContainer();
    if (engine == null) {
        return;
    }
    removeListeners(engine);
}
private void removeListeners(Container container) {
    container.removeContainerListener(this);
    container.removeLifecycleListener(this);
    for (Container child : container.findChildren()) {
        removeListeners(child);
    }
}

當(dāng)每個(gè)容器在stop()方法被調(diào)用的時(shí)候颂斜,都會(huì)觸發(fā)相應(yīng)的容器事件夫壁。我們看看ContainerBase下面觸發(fā)事件的代碼,該方法會(huì)調(diào)用所有容器監(jiān)聽(tīng)器的containerEvent()方法沃疮。

@Override
public void fireContainerEvent(String type, Object data) {
    if (listeners.size() < 1)
        return;

    ContainerEvent event = new ContainerEvent(this, type, data);
    // Note for each uses an iterator internally so this is safe
    for (ContainerListener listener : listeners) {
        listener.containerEvent(event);
    }
}

當(dāng)每個(gè)容器在stop()方法被調(diào)用的時(shí)候盒让,都會(huì)觸發(fā)相應(yīng)的生命周期事件,我們看看LifecycleBase下面觸發(fā)事件的代碼司蔬,就是調(diào)用生命周期監(jiān)聽(tīng)器的lifecycleEvent()方法

protected void fireLifecycleEvent(String type, Object data) {
    LifecycleEvent event = new LifecycleEvent(this, type, data);
    for (LifecycleListener listener : lifecycleListeners) {
        listener.lifecycleEvent(event);
    }
}

好了我們已經(jīng)看到了MapperListener接下來(lái)要分析的方法了邑茄,即:containerEvent()容器方法和lifecycleEvent()生命周期方法。

先來(lái)看containerEvent()俊啼,雖然該方法的代碼非常得長(zhǎng)肺缕,但是邏輯卻很簡(jiǎn)單。該方法有非常多的if-else(雖然我們推薦使用設(shè)計(jì)模式代替)授帕。所有的操作都是對(duì)mapper緩存的資源進(jìn)行增刪改操作同木。

@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());
    }
}

接下來(lái)看看生命周期方法,lifecycleEvent()跛十。

@Override
public void lifecycleEvent(LifecycleEvent event) {
    if (event.getType().equals(Lifecycle.AFTER_START_EVENT)) {
        Object obj = event.getSource();
        if (obj instanceof Wrapper) {
            Wrapper w = (Wrapper) obj;
            // Only if the Context has started. If it has not, then it will
            // have its own "after_start" event later.
            if (w.getParent().getState().isAvailable()) {
                registerWrapper(w);
            }
        } else if (obj instanceof Context) {
            Context c = (Context) obj;
            // Only if the Host has started. If it has not, then it will
            // have its own "after_start" event later.
            if (c.getParent().getState().isAvailable()) {
                registerContext(c);
            }
        } else if (obj instanceof Host) {
            registerHost((Host) obj);
        }
    } else if (event.getType().equals(Lifecycle.BEFORE_STOP_EVENT)) {
        Object obj = event.getSource();
        if (obj instanceof Wrapper) {
            unregisterWrapper((Wrapper) obj);
        } else if (obj instanceof Context) {
            unregisterContext((Context) obj);
        } else if (obj instanceof Host) {
            unregisterHost((Host) obj);
        }
    }
}

AFTER_START_EVENT我們前面已經(jīng)分析過(guò)了彤路,因此接下來(lái)我們主要分析分析BEFORE_STOP_EVENT。該事件可能完成對(duì)Host芥映、Context和Wrapper的取消注冊(cè)操作洲尊。我們分別來(lái)看看這3個(gè)方法。

/**
 * Unregister host.
 * 對(duì)host進(jìn)行取消注冊(cè)操作奈偏,根據(jù)hostname來(lái)remove
 */
private void unregisterHost(Host host) {
    String hostname = host.getName();

    mapper.removeHost(hostname);

    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.unregisterHost", hostname,
                domain, service));
    }
}

/**
 * Unregister context.
 * 對(duì)context取消注冊(cè)
 */
private void unregisterContext(Context context) {
    String contextPath = context.getPath();
    if ("/".equals(contextPath)) {
        contextPath = "";
    }
    String hostName = context.getParent().getName();

    if (context.getPaused()) {
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.pauseContext",
                    contextPath, service));
        }
        // 暫停的context坞嘀,不能從mapper中移除,只能在mapper暫停
        mapper.pauseContextVersion(context, hostName, contextPath,
                context.getWebappVersion());
    } else {
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("mapperListener.unregisterContext",
                    contextPath, service));
        }
        // 非暫停的context惊来,需要從mapper中移除
        mapper.removeContextVersion(context, hostName, contextPath,
                context.getWebappVersion());
    }
}

/**
 * Unregister wrapper.
 * 對(duì)wrapper取消注冊(cè)
 */
private void unregisterWrapper(Wrapper wrapper) {
    Context context = ((Context) wrapper.getParent());
    String contextPath = context.getPath();
    String wrapperName = wrapper.getName();

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

    String[] mappings = wrapper.findMappings();
    // 一個(gè)wrapper可能有多個(gè)map地址丽涩,對(duì)每個(gè)地址都需要移除操作,所以這兒是一個(gè)循環(huán)
    for (String mapping : mappings) {
        mapper.removeWrapper(hostName, contextPath, version,  mapping);
    }

    if(log.isDebugEnabled()) {
        log.debug(sm.getString("mapperListener.unregisterWrapper",
                wrapperName, contextPath, service));
    }
}

總結(jié)下來(lái)唁盏,BEFORE_STOP_EVENT在MapperListener里面有下面的功能:

  1. 對(duì)host進(jìn)行取消注冊(cè)操作内狸,根據(jù)hostname來(lái)remove
  2. 對(duì)context取消注冊(cè),暫停的context厘擂,不能從mapper中移除,只能在mapper暫停
  3. 對(duì)context取消注冊(cè)锰瘸,非暫停的context刽严,需要從mapper中移除
  4. 對(duì)wrapper取消注冊(cè),一個(gè)wrapper可能有多個(gè)map地址,對(duì)每個(gè)地址都需要移除操作舞萄,所以這兒是一個(gè)循環(huán)

3. 容器在Mapper的表現(xiàn)形式

在Mapper中眨补,所有容器都使用MapElement來(lái)表示,不同的容器有不同的子類實(shí)現(xiàn)倒脓,我們來(lái)看看類繼承層級(jí)撑螺。

MapElement類繼承層級(jí)

我們從父類MapElement開(kāi)始分析,這是一個(gè)protected修飾的抽象類崎弃,包含nameobject兩個(gè)屬性甘晤。其中object是泛型類型。

protected abstract static class MapElement<T> {
    public final String name;
    public final T object;

    public MapElement(String name, T object) {
        this.name = name;
        this.object = object;
    }
}

MappedWrapper

為了減少對(duì)MapElement子類依賴的說(shuō)明饲做,我們從MappedWrapper開(kāi)始說(shuō)明每個(gè)子類的用途线婚。

MappedWrapper中,object為Wrapper容器盆均。額外多了jsp通配符標(biāo)記是否資源標(biāo)記兩個(gè)boolean屬性塞弊。

protected static class MappedWrapper extends MapElement<Wrapper> {
    public final boolean jspWildCard;
    public final boolean resourceOnly;

    public MappedWrapper(String name, Wrapper wrapper, boolean jspWildCard,
            boolean resourceOnly) {
        super(name, wrapper);
        this.jspWildCard = jspWildCard;
        this.resourceOnly = resourceOnly;
    }
}

Context為什么需要兩個(gè)MapElement子類呢?

從類繼承層級(jí)來(lái)看泪姨,Context容器關(guān)于MapElement有兩個(gè)子類ContextVersionMappedContext游沿,為什么需要有兩個(gè)呢?

這兒不重復(fù)造輪子肮砾,參考下面的博客诀黍,我們就能知道原因。

簡(jiǎn)單來(lái)說(shuō)唇敞,tomcat從7.x版本開(kāi)始蔗草,在同一個(gè)tomcat中運(yùn)行存在一個(gè)應(yīng)用的多個(gè)版本,方便用戶進(jìn)行應(yīng)用的熱升級(jí)疆柔。

  1. 多個(gè)應(yīng)用版本是通過(guò)session來(lái)分配轉(zhuǎn)化的咒精。
  2. 假如我們有應(yīng)用appapp##1旷档,app##2這3個(gè)版本模叙,app表示最老的版本、app##1表示次老的版本鞋屈,app##2表示最新的版本范咨。
  3. 在部署app##1之前創(chuàng)建的session,在app##1部署之后厂庇,仍然會(huì)請(qǐng)求到app渠啊,直到session終結(jié)。
  4. app##1之后权旷,app##2之前創(chuàng)建的session替蛉,會(huì)請(qǐng)求到app##1
  5. app##1app##2也是有類似的處理方式。

【總結(jié)】:對(duì)應(yīng)上述的app躲查、app##1它浅、app##2,tomcat中會(huì)用3個(gè)ContextVersion對(duì)象來(lái)表示镣煮,而MappedContext是對(duì)這3個(gè)ContextVersion的數(shù)組封裝姐霍。

參考鏈接

  1. Tomcat如何部署同一應(yīng)用的不同版本
  2. tomcat多版本war應(yīng)用部署(實(shí)例講解)

ContextVersion

分析了ContextVersionMappedContext的區(qū)別和聯(lián)系,我們接著看看ContextVersion典唇,泛型類型為Context

protected static final class ContextVersion extends MapElement<Context> {
    public final String path; // contextPath镊折,上下文路徑
    public final int slashCount; // 上下文路徑的斜杠數(shù)量
    public final WebResourceRoot resources; // 根web資源
    public String[] welcomeResources; // 歡迎資源列表
    public MappedWrapper defaultWrapper = null; // 默認(rèn)wrapper
    public MappedWrapper[] exactWrappers = new MappedWrapper[0]; // 準(zhǔn)確wrapper列表
    public MappedWrapper[] wildcardWrappers = new MappedWrapper[0]; // 通配符wrapper列表
    public MappedWrapper[] extensionWrappers = new MappedWrapper[0]; // 擴(kuò)展wrapper列表
    public int nesting = 0; // wrapper嵌套層次,用于表示context下wrapper列表中最大的斜杠數(shù)
    private volatile boolean paused; // 暫停標(biāo)記

    public ContextVersion(String version, String path, int slashCount,
            Context context, WebResourceRoot resources,
            String[] welcomeResources) {
        super(version, context);
        this.path = path;
        this.slashCount = slashCount;
        this.resources = resources;
        this.welcomeResources = welcomeResources;
    }

    public boolean isPaused() {
        return paused;
    }

    public void markPaused() {
        paused = true;
    }
}

MappedContext

一個(gè)MappedContextContextVersion[]數(shù)組的包裝蚓聘。

protected static final class MappedContext extends MapElement<Void> {
    public volatile ContextVersion[] versions;

    public MappedContext(String name, ContextVersion firstVersion) {
        super(name, null);
        this.versions = new ContextVersion[] { firstVersion };
    }
}

ContextList

ContextListMappedContext[]數(shù)組的封裝腌乡。同時(shí)提供對(duì)MappedContext的新增和刪除操作。

protected static final class ContextList {

    public final MappedContext[] contexts;
    public final int nesting;

    public ContextList() {
        this(new MappedContext[0], 0);
    }

    private ContextList(MappedContext[] contexts, int nesting) {
        this.contexts = contexts;
        this.nesting = nesting;
    }

    public ContextList addContext(MappedContext mappedContext,
            int slashCount) {
        MappedContext[] newContexts = new MappedContext[contexts.length + 1];
        if (insertMap(contexts, newContexts, mappedContext)) {
            return new ContextList(newContexts, Math.max(nesting,
                    slashCount));
        }
        return null;
    }

    public ContextList removeContext(String path) {
        MappedContext[] newContexts = new MappedContext[contexts.length - 1];
        if (removeMap(contexts, newContexts, path)) {
            int newNesting = 0;
            for (MappedContext context : newContexts) {
                newNesting = Math.max(newNesting, slashCount(context.name));
            }
            return new ContextList(newContexts, newNesting);
        }
        return null;
    }
}

MappedHost

該方法有一些重要的屬性和特征夜牡。包括:

  1. ContextList contextList与纽,上下文列表
  2. MappedHost realHost
    • 真實(shí)的host,一個(gè)主機(jī)可能有多個(gè)別名塘装。
    • 所有別名的MappedHost共享一個(gè)非別名的MappedHost急迂,并存放在這個(gè)屬性中
  3. List<MappedHost> aliases
    • 別名MappedHost列表
    • 為了統(tǒng)一處理和簡(jiǎn)單使用蹦肴,這個(gè)字段只會(huì)在非別名的MappedHost才有值
    • 別名的MappedHost僚碎,該屬性為null
protected static final class MappedHost extends MapElement<Host> {

    public volatile ContextList contextList;

    /**
     * Link to the "real" MappedHost, shared by all aliases.
     * 真實(shí)的host,一個(gè)主機(jī)可能有多個(gè)別名阴幌。
     * 所有`別名的MappedHost`共享一個(gè)`非別名的MappedHost`勺阐,并存放在這個(gè)屬性中
     */
    private final MappedHost realHost;

    /**
     * Links to all registered aliases, for easy enumeration. This field
     * is available only in the "real" MappedHost. In an alias this field
     * is <code>null</code>.
     * 1. `別名MappedHost列表`。
     * 2. 為了統(tǒng)一處理和簡(jiǎn)單使用矛双,這個(gè)字段只會(huì)在`非別名的MappedHost`才有值
     * 3. `別名的MappedHost`渊抽,該屬性為null
     */
    private final List<MappedHost> aliases;

    /**
     * Constructor used for the primary Host
     *
     * @param name The name of the virtual host
     * @param host The host
     */
    public MappedHost(String name, Host host) {
        super(name, host);
        realHost = this;
        contextList = new ContextList();
        aliases = new CopyOnWriteArrayList<>();
    }

    /**
     * Constructor used for an Alias
     *
     * @param alias    The alias of the virtual host
     * @param realHost The host the alias points to
     */
    public MappedHost(String alias, MappedHost realHost) {
        super(alias, realHost.object);
        this.realHost = realHost;
        this.contextList = realHost.contextList;
        this.aliases = null;
    }

    public boolean isAlias() {
        return realHost != this;
    }

    public MappedHost getRealHost() {
        return realHost;
    }

    public String getRealHostName() {
        return realHost.name;
    }

    public Collection<MappedHost> getAliases() {
        return aliases;
    }

    public void addAlias(MappedHost alias) {
        aliases.add(alias);
    }

    public void addAliases(Collection<? extends MappedHost> c) {
        aliases.addAll(c);
    }

    public void removeAlias(MappedHost alias) {
        aliases.remove(alias);
    }
}

4. MapElement的新增、刪除和查詢操作

查找相關(guān)方法主要為下面幾個(gè):

private static final <T, E extends MapElement<T>> E exactFind(E[] map, CharChunk name)
private static final <T, E extends MapElement<T>> E exactFind(E[] map, String name)
private static final <T, E extends MapElement<T>> E exactFindIgnoreCase(E[] map, CharChunk name)
private static final <T> int find(MapElement<T>[] map, CharChunk name)
private static final <T> int find(MapElement<T>[] map, CharChunk name, int start, int end)
private static final <T> int find(MapElement<T>[] map, String name)
private static final <T> int findIgnoreCase(MapElement<T>[] map, CharChunk name)
private static final <T> int findIgnoreCase(MapElement<T>[] map, CharChunk name, int start, int end)

在Mapper里面议忽,查詢一共有下面三大類方法:

  1. exactFind(xxx)懒闷,查找并提取出name相同的節(jié)點(diǎn)
  2. find,查找并返回name相同的節(jié)點(diǎn)的下標(biāo)
  3. findIgnoreCase栈幸,查找并返回name相同(忽略大小寫(xiě))的節(jié)點(diǎn)的下標(biāo)

這3類方法都很相似愤估,這兒我們僅僅分析其中的一個(gè)方法。

private static final <T> int find(MapElement<T>[] map, String name) {
    // a表示開(kāi)始位置速址,默認(rèn)為0玩焰,表示從第一個(gè)開(kāi)始;b表示結(jié)束位置芍锚,默認(rèn)為長(zhǎng)度-1
    int a = 0;
    int b = map.length - 1;

    // b == -1表示map里面沒(méi)有元素震捣,返回-1表示未查詢到
    // Special cases: -1 and 0
    if (b == -1) {
        return -1;
    }

    // 名字比map里面的第一個(gè)元素還小荔棉,返回-1表示未查詢到
    if (name.compareTo(map[0].name) < 0) {
        return -1;
    }
    // b == 0表示名字比第一個(gè)元素大闹炉,但還是未查詢到
    if (b == 0) {
        return 0;
    }

    // 其他情況蒿赢,則使用二分查找法來(lái)查詢?cè)叵聵?biāo)
    int i = 0;
    while (true) {
        i = (b + a) / 2;
        int result = name.compareTo(map[i].name);
        if (result > 0) {
            a = i;
        } else if (result == 0) {
            return i;
        } else {
            b = i;
        }
        if ((b - a) == 1) {
            int result2 = name.compareTo(map[b].name);
            if (result2 < 0) {
                return a;
            } else {
                return b;
            }
        }
    }
}

分析完了查找,我們繼續(xù)分析刪除渣触,只有一個(gè)方法羡棵。先查到name匹配的節(jié)點(diǎn)的下標(biāo)pos,pos之前和之后的元素都拷貝到一個(gè)新數(shù)組里面嗅钻,然后返回true表示刪除成功皂冰。

/**
 * Insert into the right place in a sorted MapElement array.
 */
private static final <T> boolean removeMap
    (MapElement<T>[] oldMap, MapElement<T>[] newMap, String name) {
    int pos = find(oldMap, name);
    if ((pos != -1) && (name.equals(oldMap[pos].name))) {
        System.arraycopy(oldMap, 0, newMap, 0, pos);
        System.arraycopy(oldMap, pos + 1, newMap, pos,
                         oldMap.length - pos - 1);
        return true;
    }
    return false;
}

最后我們來(lái)分析新增方法,也只有一個(gè)方法养篓,即:insertMap()秃流。該方法做如下操作:

  1. 通過(guò)二分查找的方式查找節(jié)點(diǎn)名字在oldMap中最近的位置。為啥是最近的呢柳弄?是因?yàn)榇迦氲脑貢?huì)將后面的元素往后擠一位舶胀。
  2. oldMap里面存在同名的節(jié)點(diǎn),則不會(huì)插入碧注,對(duì)外返回false
  3. 數(shù)組拷貝到newMap中嚣伐,并將新節(jié)點(diǎn)插入到查找到的位置。這兒需要注意萍丐,插入前后的數(shù)組都是有序的轩端!
/**
 * 正確插入元素到一個(gè)有序的數(shù)組,并且防止重復(fù)插入
 *
 * Insert into the right place in a sorted MapElement array, and prevent
 * duplicates.
 */
private static final <T> boolean insertMap
    (MapElement<T>[] oldMap, MapElement<T>[] newMap, MapElement<T> newElement) {
    // 通過(guò)二分查找的方式查找節(jié)點(diǎn)名字在oldMap中最近的位置
    int pos = find(oldMap, newElement.name);
    // oldMap里面存在同名的節(jié)點(diǎn)逝变,則不會(huì)插入基茵,對(duì)外返回false
    if ((pos != -1) && (newElement.name.equals(oldMap[pos].name))) {
        return false;
    }
    // 數(shù)組拷貝到newMap中,并將新節(jié)點(diǎn)插入到查找到的位置壳影。
    // 這兒需要注意拱层,插入前后的數(shù)組都是有序的!
    System.arraycopy(oldMap, 0, newMap, 0, pos + 1);
    newMap[pos + 1] = newElement;
    System.arraycopy
        (oldMap, pos + 1, newMap, pos + 2, oldMap.length - pos - 1);
    return true;
}

5. 容器的新增态贤、刪除和查詢操作

我們先來(lái)看看addHost方法舱呻。主要做下面事情:

  1. 數(shù)組擴(kuò)容,長(zhǎng)度為原數(shù)組長(zhǎng)度+1
  2. 插入成功之后悠汽,將再次判斷新節(jié)點(diǎn)名字和默認(rèn)主機(jī)名是否一致箱吕,如果是,則將新節(jié)點(diǎn)設(shè)置為默認(rèn)主機(jī)柿冲。
  3. 插入失敗茬高,說(shuō)明host已經(jīng)存在于hosts里面了,如果是這種情況假抄,則該方法提供冪等支持怎栽。否則需要找出重復(fù)的節(jié)點(diǎn)丽猬,并記錄錯(cuò)誤日志然后返回
  4. 如果host有多個(gè)別名,需要針對(duì)每個(gè)別名生成MappedHost對(duì)象熏瞄,并放入主名MappedHost的aliases列表中
/**
 * Add a new host to the mapper.
 * 添加一個(gè)新的host到mapper
 *
 * @param name Virtual host name
 * @param aliases Alias names for the virtual host
 * @param host Host object
 */
public synchronized void addHost(String name, String[] aliases,
                                 Host host) {
    name = renameWildcardHost(name);
    // 數(shù)組擴(kuò)容脚祟,長(zhǎng)度為原數(shù)組長(zhǎng)度+1
    MappedHost[] newHosts = new MappedHost[hosts.length + 1];
    MappedHost newHost = new MappedHost(name, host);

    // 插入成功之后,將再次判斷新節(jié)點(diǎn)名字和默認(rèn)主機(jī)名是否一致强饮,如果是由桌,則將新節(jié)點(diǎn)設(shè)置為默認(rèn)主機(jī)。
    if (insertMap(hosts, newHosts, newHost)) {
        hosts = newHosts;
        if (newHost.name.equals(defaultHostName)) {
            defaultHost = newHost;
        }
        if (log.isDebugEnabled()) {
            log.debug(sm.getString("mapper.addHost.success", name));
        }
    }

    // 插入失敗邮丰,說(shuō)明host已經(jīng)存在于hosts里面了行您,如果是這種情況,則該方法提供冪等支持剪廉。否則需要找出重復(fù)的節(jié)點(diǎn)娃循,并記錄錯(cuò)誤日志然后返回
    else {
        MappedHost duplicate = hosts[find(hosts, name)];
        if (duplicate.object == host) {
            // The host is already registered in the mapper.
            // E.g. it might have been added by addContextVersion()
            if (log.isDebugEnabled()) {
                log.debug(sm.getString("mapper.addHost.sameHost", name));
            }
            newHost = duplicate;
        } else {
            log.error(sm.getString("mapper.duplicateHost", name,
                    duplicate.getRealHostName()));
            // Do not add aliases, as removeHost(hostName) won't be able to
            // remove them
            return;
        }
    }

    // 如果host有多個(gè)別名,需要針對(duì)每個(gè)別名生成MappedHost對(duì)象斗蒋,并放入主名MappedHost的aliases列表中
    List<MappedHost> newAliases = new ArrayList<>(aliases.length);
    for (String alias : aliases) {
        alias = renameWildcardHost(alias);
        MappedHost newAlias = new MappedHost(alias, newHost);
        if (addHostAliasImpl(newAlias)) {
            newAliases.add(newAlias);
        }
    }
    newHost.addAliases(newAliases);
}

ContextWrapper新增的入口都在addContextVersion()捌斧,我們從這個(gè)方法開(kāi)始分析。該方法實(shí)現(xiàn)得非常復(fù)雜吹泡,邏輯也比較難懂骤星,如若發(fā)現(xiàn)樓主有理解偏差,請(qǐng)大家不吝賜教爆哑!

實(shí)現(xiàn)的功能大致如下:

  1. 重新生成主機(jī)名
  2. 如果主機(jī)不在主機(jī)列表中洞难,則添加到主機(jī)列表。添加之后仍然找不到揭朝,則記錄錯(cuò)誤日志队贱,并終止對(duì)context的添加
  3. 如果映射的主機(jī)為別名主機(jī),則認(rèn)為host不存在
  4. 如果context下面有wrapper潭袱,則將其下的所有wrapper也添加到mapper
  5. 在mapper中柱嫌,contextPath可以看成是context的名字,所以這兒使用contextPath來(lái)查找
    1. 沒(méi)有找到屯换,則說(shuō)明是首次新增context编丘,版本由方法調(diào)用時(shí)傳入。會(huì)做以下操作:
      1. 將其添加到MappedHost下面的context列表
      2. 將其添加到mapper的contextObjectToContextVersionMap中
    2. 找到了彤悔,也需要嘗試重新插入嘉抓。因?yàn)榭赡艽嬖诓煌姹镜腸ontext。
      1. 若插入contextVersion成功晕窑,則版本列表更新為擴(kuò)容后的version數(shù)組
      2. 若沒(méi)有插入convertVersion成功抑片,則執(zhí)行"覆蓋"的相關(guān)操作
public void addContextVersion(String hostName, Host host, String path,
        String version, Context context, String[] welcomeResources,
        WebResourceRoot resources, Collection<WrapperMappingInfo> wrappers) {

    // 重新生成主機(jī)名
    hostName = renameWildcardHost(hostName);

    // 如果主機(jī)不在主機(jī)列表中,則添加到主機(jī)列表
    MappedHost mappedHost  = exactFind(hosts, hostName);
    if (mappedHost == null) {
        addHost(hostName, new String[0], host);
        // 添加之后仍然找不到杨赤,則記錄錯(cuò)誤日志敞斋,并終止對(duì)context的添加
        mappedHost = exactFind(hosts, hostName);
        if (mappedHost == null) {
            log.error("No host found: " + hostName);
            return;
        }
    }
    // 如果映射的主機(jī)為別名主機(jī)截汪,則認(rèn)為host不存在
    if (mappedHost.isAlias()) {
        log.error("No host found: " + hostName);
        return;
    }
    // 獲取contextPath路徑的斜杠數(shù)
    int slashCount = slashCount(path);

    synchronized (mappedHost) {
        ContextVersion newContextVersion = new ContextVersion(version,
                path, slashCount, context, resources, welcomeResources);
        // 如果context下面有wrapper,則將其下的所有wrapper也添加到mapper
        if (wrappers != null) {
            addWrappers(newContextVersion, wrappers);
        }

        ContextList contextList = mappedHost.contextList;
        // 在mapper中植捎,contextPath可以看成是context的名字衙解,所以這兒使用contextPath來(lái)查找
        MappedContext mappedContext = exactFind(contextList.contexts, path);

        // 沒(méi)有找到,則說(shuō)明是首次新增context鸥跟,版本由方法調(diào)用時(shí)傳入丢郊。會(huì)做以下操作:
        // 1. 將其添加到MappedHost下面的context列表
        // 2. 將其添加到mapper的contextObjectToContextVersionMap中
        if (mappedContext == null) {
            mappedContext = new MappedContext(path, newContextVersion);
            ContextList newContextList = contextList.addContext(
                    mappedContext, slashCount);
            if (newContextList != null) {
                updateContextList(mappedHost, newContextList);
                contextObjectToContextVersionMap.put(context, newContextVersion);
            }
        }

        // 找到了,也需要嘗試重新插入医咨。因?yàn)榭赡艽嬖诓煌姹镜腸ontext。
        // 1. 若插入contextVersion成功架诞,則版本列表更新為擴(kuò)容后的version數(shù)組
        // 2. 若沒(méi)有插入convertVersion成功拟淮,則執(zhí)行"覆蓋"的相關(guān)操作
        else {
            ContextVersion[] contextVersions = mappedContext.versions;
            ContextVersion[] newContextVersions = new ContextVersion[contextVersions.length + 1];
            if (insertMap(contextVersions, newContextVersions,
                    newContextVersion)) {
                mappedContext.versions = newContextVersions;
                contextObjectToContextVersionMap.put(context, newContextVersion);
            } else {
                // Re-registration after Context.reload()
                // Replace ContextVersion with the new one
                // 找到了且名字相同,則認(rèn)為是reload重新加載操作谴忧,則需要對(duì)context進(jìn)行覆蓋操作
                int pos = find(contextVersions, version);
                if (pos >= 0 && contextVersions[pos].name.equals(version)) {
                    contextVersions[pos] = newContextVersion;
                    contextObjectToContextVersionMap.put(context, newContextVersion);
                }
            }
        }
    }
}

接下來(lái)很泊,我們分析一下新增wrapper的方法addWrapper()。該方法也很長(zhǎng)沾谓,但是做的事情卻很清楚委造。

  1. 以"/*"結(jié)尾,則認(rèn)為是通配符wrapper均驶,需要將其加到context的wildcardWrappers列表中
  2. 以"*."開(kāi)頭昏兆,則認(rèn)為是擴(kuò)展wrapper,需要將其加到context的extensionWrappers列表中
  3. 路徑等于"/"妇穴,則認(rèn)為是默認(rèn)的wrapper爬虱,直接賦值為defaultWrapper
  4. 其他情況則認(rèn)為是精確wrapper,需要將其加到context的exactWrappers列表中
protected void addWrapper(ContextVersion context, String path,
        Wrapper wrapper, boolean jspWildCard, boolean resourceOnly) {

    synchronized (context) {
        // 以"/*"結(jié)尾腾它,則認(rèn)為是通配符wrapper跑筝,需要將其加到context的wildcardWrappers列表中
        if (path.endsWith("/*")) {
            // Wildcard wrapper
            String name = path.substring(0, path.length() - 2);
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.wildcardWrappers;
            MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.wildcardWrappers = newWrappers;
                int slashCount = slashCount(newWrapper.name);
                if (slashCount > context.nesting) {
                    context.nesting = slashCount;
                }
            }
        }

        // 以"*."開(kāi)頭,則認(rèn)為是擴(kuò)展wrapper瞒滴,需要將其加到context的extensionWrappers列表中
        else if (path.startsWith("*.")) {
            // Extension wrapper
            String name = path.substring(2);
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.extensionWrappers;
            MappedWrapper[] newWrappers =
                new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.extensionWrappers = newWrappers;
            }
        }

        // 路徑等于"/"曲梗,則認(rèn)為是默認(rèn)的wrapper,直接賦值為defaultWrapper
        else if (path.equals("/")) {
            // Default wrapper
            MappedWrapper newWrapper = new MappedWrapper("", wrapper,
                    jspWildCard, resourceOnly);
            context.defaultWrapper = newWrapper;
        }

        // 其他情況則認(rèn)為是精確wrapper妓忍,需要將其加到context的exactWrappers列表中
        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;
            }
            MappedWrapper newWrapper = new MappedWrapper(name, wrapper,
                    jspWildCard, resourceOnly);
            MappedWrapper[] oldWrappers = context.exactWrappers;
            MappedWrapper[] newWrappers = new MappedWrapper[oldWrappers.length + 1];
            if (insertMap(oldWrappers, newWrappers, newWrapper)) {
                context.exactWrappers = newWrappers;
            }
        }
    }
}

接下來(lái)我們分析一下容器查詢的方法-findContextVersion()虏两。首先查找host,然后查找context单默,最后查找contextVersion碘举。

private ContextVersion findContextVersion(String hostName,
        String contextPath, String version, boolean silent) {
    // 查找host
    MappedHost host = exactFind(hosts, hostName);
    if (host == null || host.isAlias()) {
        if (!silent) {
            log.error("No host found: " + hostName);
        }
        return null;
    }
    // 查找context
    MappedContext context = exactFind(host.contextList.contexts,
            contextPath);
    if (context == null) {
        if (!silent) {
            log.error("No context found: " + contextPath);
        }
        return null;
    }
    // 查找contextVersion
    ContextVersion contextVersion = exactFind(context.versions, version);
    if (contextVersion == null) {
        if (!silent) {
            log.error("No context version found: " + contextPath + " "
                    + version);
        }
        return null;
    }
    return contextVersion;
}

容器相關(guān)的remove()方法是add()方法的逆向操作。remove()方法和add()方法比較類似搁廓,代碼也比較多引颈。限于篇幅耕皮,本文不再詳細(xì)描述!

至此蝙场,我們基本分析完了Mapper中容器的新增凌停、刪除和查詢操作。

6. Mapper.map()方法

【注】:該方法雖然隸屬于Mapper售滤,但是里面的邏輯非常復(fù)雜罚拟,我們留待第十小節(jié)來(lái)分析!

總結(jié)

本文詳細(xì)分析了MapperListenerMapper完箩。MapperListener通過(guò)監(jiān)聽(tīng)容器事件來(lái)完成對(duì)容器的注冊(cè)和取消注冊(cè)赐俗。而Mapper用于對(duì)容器進(jìn)行緩存和管理,同時(shí)提供uri映射的功能弊知。在tomcat中這兩個(gè)組件非常非常地重要阻逮,代碼也非常多,功能也很復(fù)雜秩彤,要想完全弄懂叔扼,還是需要深入到代碼里面去。

不過(guò)漫雷,樓主相信本文已經(jīng)將這兩個(gè)組件的設(shè)計(jì)和實(shí)現(xiàn)給講清楚了瓜富。代碼里面,各種設(shè)計(jì)模式降盹、數(shù)據(jù)結(jié)構(gòu)和編程思想滿天飛与柑。閱讀完了他們的代碼,我們很容易發(fā)現(xiàn)澎现,大牛的軟技能和架構(gòu)設(shè)計(jì)的功力可是相當(dāng)?shù)纳詈窠霭8袊@驚訝的同時(shí),自己也開(kāi)闊了眼界剑辫,樓主可謂是受益匪淺~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末干旧,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子妹蔽,更是在濱河造成了極大的恐慌椎眯,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件胳岂,死亡現(xiàn)場(chǎng)離奇詭異编整,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)乳丰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門掌测,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人产园,你說(shuō)我怎么就攤上這事汞斧∫褂簦” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵粘勒,是天一觀的道長(zhǎng)竞端。 經(jīng)常有香客問(wèn)我,道長(zhǎng)庙睡,這世上最難降的妖魔是什么事富? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮乘陪,結(jié)果婚禮上统台,老公的妹妹穿的比我還像新娘。我一直安慰自己暂刘,他們只是感情好饺谬,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著谣拣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪族展。 梳的紋絲不亂的頭發(fā)上森缠,一...
    開(kāi)封第一講書(shū)人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音仪缸,去河邊找鬼贵涵。 笑死,一個(gè)胖子當(dāng)著我的面吹牛恰画,可吹牛的內(nèi)容都是我干的宾茂。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼拴还,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼跨晴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起片林,我...
    開(kāi)封第一講書(shū)人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤端盆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后费封,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體焕妙,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年弓摘,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了焚鹊。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡韧献,死狀恐怖末患,靈堂內(nèi)的尸體忽然破棺而出研叫,到底是詐尸還是另有隱情,我是刑警寧澤阻塑,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布蓝撇,位于F島的核電站,受9級(jí)特大地震影響陈莽,放射性物質(zhì)發(fā)生泄漏渤昌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一走搁、第九天 我趴在偏房一處隱蔽的房頂上張望独柑。 院中可真熱鬧,春花似錦私植、人聲如沸忌栅。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)索绪。三九已至,卻和暖如春贫悄,著一層夾襖步出監(jiān)牢的瞬間瑞驱,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工窄坦, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留唤反,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓鸭津,卻偏偏與公主長(zhǎng)得像彤侍,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子逆趋,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

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

  • 從三月份找實(shí)習(xí)到現(xiàn)在盏阶,面了一些公司,掛了不少父泳,但最終還是拿到小米般哼、百度、阿里惠窄、京東蒸眠、新浪、CVTE杆融、樂(lè)視家的研發(fā)崗...
    時(shí)芥藍(lán)閱讀 42,184評(píng)論 11 349
  • Spring Cloud為開(kāi)發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見(jiàn)模式的工具(例如配置管理楞卡,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,599評(píng)論 18 139
  • 前言 在Tomcat中蒋腮,容器(Container)主要包括四種淘捡,Engine、Host池摧、Context和Wrapp...
    juconcurrent閱讀 3,454評(píng)論 1 5
  • 原文: 采采卷耳①焦除,不盈頃筐②。 嗟我懷人作彤,置彼周行③膘魄。 陟④彼崔嵬⑤,我馬虺隤⑥竭讳。 我姑酌彼金罍⑦创葡,維以不永懷⑧...
    弱德之美閱讀 341評(píng)論 0 0
  • 我怎么如此幸運(yùn),今天早上五點(diǎn)多醒來(lái)绢慢,打開(kāi)簡(jiǎn)書(shū)灿渴,準(zhǔn)備寫(xiě)意想不到,卻在簡(jiǎn)書(shū)的首頁(yè)看到了能引起我興趣的題目胰舆,打開(kāi)這些作者...
    國(guó)粹堂1閱讀 223評(píng)論 0 0