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

简述

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

Container

【深入剖析Tomcat笔记】第五篇 Tomcat Container和PipeliningTasks_第1张图片
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 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

【深入剖析Tomcat笔记】第五篇 Tomcat Container和PipeliningTasks_第2张图片
Pipeline.png

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

【深入剖析Tomcat笔记】第五篇 Tomcat Container和PipeliningTasks_第3张图片
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 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。

你可能感兴趣的:(【深入剖析Tomcat笔记】第五篇 Tomcat Container和PipeliningTasks)