【深入剖析Tomcat笔记】第五篇 Tomcat Container和PipeliningTasks

简述

在第一章开始就提到,Tomcat的本质什么?是容器。容器这个概念现在很火,一提到容器,我们立马可以想到虚拟化,SAAS,Docket。虽然在这里,我们不会深入去探究其他虚拟化相关技术中的容器思想,但Tomcat的容器究竟是什么,容器为Tomcat带来了什么,这次我们一起去探究。

Container

ContainerStructure.png

org.apache.catalina.Container 接口定义了容器的形式,有四种容器:

  • Engine[引擎]:tomcat顶级容器,包含所有servlet,可包含多个Host
  • Host[虚拟主机]:可包含多个Context
  • Context[上下文]:包含一个Web应用,包含一个或多个wrapper
  • Wrapper[包装器]:一个单独Servlet的 ServletProcessor容器

REST设计中引入一个很重要的概念,资源。Servlet对数据的解析,本质上也是一种对资源的转换。例如,查询某一条记录,输入一组特别标志,获得特定标志对应的数据,这相当将特定标志转化为数据,这是一种资源的转化;通过URL获取一个静态页面,url地址同样是一种请求输入,而静态页面作为URL对应的静态输出,这同样是一种资源的转化。Tomcat中对于资源的转化是由servlet完成的,Tomcat很少直接参与资源的转化,Tomcat给予Servlet资源转化最主要的是提供了完善的环境,而这种环境主要就是容器Container所提供的。

Engine是所有资源的总入口,提供了整体的容器,通过Engine可以实现多个Host虚拟主机同时存在。

Host虚拟主机为Tomcat提供了多虚拟主机配置的功能,将一个Tomcat通过虚拟主机配置,可以划分成为逻辑独立的多台虚拟服务容器。通过虚拟主机,可以实现多域名映射多虚拟主机,虚拟主机独立数据存储,虚拟主机独立配置等。

Context为服务提供容器,每一个在tomcat部署的服务都由一个Context提供服务,通过Context容器,Tomcat为我们提供了很多有趣的特性,其中很重要的一个特性就是Session和Cookie。HTTP虽然是无状态协议,但是通过Session和Cookie,HTTP协议实现了真正拥有上下文的交互方式。通过上下文,我们实现了有状态的资源交互。更重要的一点是,这种有趣的交互方式由容器来实现,而服务无需关心其实现。
Wrapper为每一个Servlet提供一个对应的容器。

一个父容器可以包含一个或多个子容器,如一个Engine可以设置多个Host。
很明显,Contianer其实是一种组合模式(Composite Pattern)。

将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
《设计模式--组合模式定义》

在《设计模式》中对于组合模式实现的时候需要关注的几个点:

  1. 显式父部件引用
  2. 最大化Component接口
  3. 声明管理子部件的操作

因此设计Container接口如下

    //添加子容器
    public void addChild(Container child);

    //移除子容器
    public void removeChild(Container child);

    //查找子容器
    public Container findChild(String childName);

    //子容器列表
    public Container[] findChildren();

    //获取父容器
    public Container getParent();

    //设置父容器
    public void setParent(Container parent);

    //获取容器名称
    public String getName();


    //设置容器名称
    public void setName(String name);

根据Container接口,设计ContainerBase抽象类如下:

public abstract class ContainerBase implements Container {

    /**
     * 子容器存储
     */
    private HashMap<String, Container> children = new HashMap();

    /**
     * Name
     */
    private String name ;

    /**
     * 父容器
     */
    private Container parent;


    /**
     * 添加子容器
     * @param child Container
     */
    @Override
    public void addChild(Container child) {

        children.put(child.getName() ,child);
    }

    /**
     * 移除子容器
     * @param child Container
     */
    @Override
    public void removeChild(Container child) {
        children.remove(child);
    }

    /**
     * 查找子容器
     * @param childName
     */
    @Override
    public Container findChild(String childName) {
        return (Container) children.get(childName);
    }

    /**
     * 获取所有子容器
     * @return Contianer[]
     */
    @Override
    public Container[] findChildren() {
        return (Container[]) children.values().toArray();
    }

    /**
     * 设置父容器
     * @param parent Container
     */
    @Override
    public void setParent(Container parent) {
        this.parent.removeChild(this);
        this.parent = parent;
    }

    /**
     * 获取父容器
     * @return Container
     */
    @Override
    public Container getParent() {
        return parent;
    }

    /**
     * 获取容器名称
     * @return String name
     */
    @Override
    public String getName() {
        return this.name;
    }


    /**
     * 设置容器名称
     */
    @Override
    public void setName(String name) {
        this.parent.removeChild(this);
        this.name = name;
        this.parent.addChild(this);
    }
}

Engine、Host和Context从本质上来说都属于上层容器,而在这里我们通过Wrapper实现对于ServletProcessor的封装及管理。设计代码如下


PipeliningTasks和Valve

相信大家都接触过拦截器和Spring AOP,实际上Pipelining Tasks也是一种AOP思想的实现方式。

Tomcat通过Pipeline和Valve实现上述问题。

Pipeline

Pipeline.png

Pipeline整体结构如上所示。
Pipeline 设计非常形象,不知道大家有没有玩过接水管,实际上Pipeline正如管道所示。一个管道前可以设置多个阀门,但至少设置一个基础阀门。
上一节中我们讲到Context可以包含多个Wrapper,在引入Pipeline之后的结构正如下图所示。Context包含多个Wrapper,在执行每一个Wrapper之前,先要过Context的Pipeline,同时每一个Wrapper都具有属于自己的Pipeline。

QQ截图20170731203055.png

我们对Pipeline进行分析,可以设计基本Pipeline接口如下

//设置基本阀门
public void setBasic(Valve basic);

//获取基本阀门
public Valve getBasic();

//添加阀门
public void addValve(Valve valve);

//删除阀门
public void remove(Valve valve);

//获得所有非基本阀门
public Valve[] getValves();

//下节说明
public void invoke(Request request, Response response);

同理,Engine、Host、Context和Wrapper经过Pipeline可以实现类似于AOP的配置,而这个配置就是上图中的“阀门”。那么Tomcat中如何实现阀门功能以及阀门的配置呢?请移步下节

Valve 和ValveContext

Valve和ValveContext实际上实现了Tomcat对于阀门的控制,先看一下Valve和ValveContext的结构设计。

Valve

//Valve信息
public String getInfo() ;

//invoke
public void invoke(Request request, Response response, ValveContext context) throws IOException;


ValveContext


//ValveContext信息
public String getInfo();

//向下移动
public void invokeNext(Request request, Response response)
        throws IOException, ServletException;

我们可以看到,Valve对象invoke方法中包含ValveContext。实际上ValveContext接口似于现在使用的Iterator接口。与Iterator接口不同的是,Iterator接口将集合对象的控制权集中于接口实现类内部,通过invoke内部对对象进行控制,而ValveContext是将控制权转交给Valve进行使用。并且Valve相较Iterator更为精简,只保留需要的功能。


//比较简单,很好理解,结构图中有
public interface Contained {

    public void setContainer(Container container);

    public Container getContainer();
}


**
 * StandardPipeline
 * Created by admin on 2017/8/1.
 */
public class StandardPipeline implements Pipeline, Contained {

    private Container container;

    private Valve basicValve;

    private List<Valve> valves = new ArrayList<>();


    @Override
    public Container getContainer() {
        return this.container;
    }

    @Override
    public void setContainer(Container container) {
        this.container = container;
    }

    @Override
    public void setBasic(Valve basic) {
        this.basicValve = basic;
    }

    @Override
    public Valve getBasic() {
        return this.basicValve;
    }

    @Override
    public void addValve(Valve valve) {
        valves.add(valve);
    }

    @Override
    public void remove(Valve valve) {
        valves.remove(valve);
    }

    @Override
    public Valve[] getValves() {
        return (Valve[]) valves.toArray();
    }

    @Override
    public void invoke(Request request, Response response) throws IOException, ServletException {

        // Invoke the first Valve in this pipeline for this request
        (new StanardValveContext()).invokeNext(request, response);
    }


    protected class StanardValveContext implements ValveContext {

        protected String info = "com.cunchen.core.StandardPipeline/1.0";

        protected int stage = 0;


        @Override
        public String getInfo() {
            return info;
        }

        @Override
        public void invokeNext(Request request, Response response) throws IOException, ServletException {
            int subscript = stage;
            stage = stage + 1;

            // Invoke the requested Valve for the current request thread
            if (subscript < valves.size()) {
                valves.get(subscript).invoke(request, response, this);
            } else if ((subscript == valves.size()) && (basicValve != null)) {
                basicValve.invoke(request, response, this);
            } else {
                throw new ServletException("standardPipeline.noValve");
            }
        }
    }
}

/**
 * 客户请求IP记录阀DEMO
 * Created by wqd on 2017/3/9.
 */
public class ClientIPLoggerValve implements Valve, Contained {

    protected Container container;

    protected String info;

    private Logger log = Logger.getLogger(info);

    @Override
    public Container getContainer() {
        return this.container;
    }

    @Override
    public void setContainer(Container container) {
        this.container = container;
    }

    @Override
    public String getInfo() {
        return info;
    }

    /**
     * 代理方法
     * @param request {@link Request}
     * @param response {@link Response}
     * @param valveContext {@link ValveContext}
     * @throws IOException Valve.invoke
     * @throws ServletException Valve.invoke
     */
    @Override
    public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException {

        valveContext.invokeNext(request, response);


        ServletRequest request1 = request.getRequest();
        if(request1 instanceof HttpServletRequest) {
            HttpServletRequest hreq = (HttpServletRequest) request1;
            Enumeration headerNames = hreq.getHeaderNames();
            while(headerNames.hasMoreElements()) {
                String headerName = headerNames.nextElement().toString();

                String headerValue = hreq.getHeader(headerName);

                log.info(getInfo() + "recorder-----" + headerName + ":"  + headerValue);
            }
        }

    }

}

从代码来看,不难理解作者其实想通过ValveContext接口,可以扩展实现不同的上下文控制类。但实际上,ValveContext这种设计最直接的造成了Valve对象的代码冗余(需要Valve.invoke执行后,调用ValveContext.nextInvoke),并且实现同样的功能完全可以通过控制集合类实现。因此在Tomcat 5以后,valveContext接口被取消,Valve接口增加setNext()方法,将Valve结构改为链表形式,有兴趣的小伙伴可以查看Tomcat 7 StandardPipeline

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

推荐阅读更多精彩内容