Tomcat剖析(五):Tomcat 容器

Tomcat剖析(五):Tomcat 容器

第一部分:概述

这一节基于《深度剖析Tomcat》第五章:容器 总结而成html

必定要先到个人github上下载本书相关的代码,同时去上网下载这本书。java

看了不懂的欢迎加我QQ一块儿探讨或者在博客下评论发问。git

时隔两个多月,中间包括了学校的期末考试月和一些琐粹的事情。github

这一部分会先给你们先总结一下Tomcat链接器的相关内容,而后是 Tomcat 容器的整体介绍,最后在下一部分进入本章的主题: Tomcat 容器 的详细讲解。web

Tomcat 链接器总结

咱们都知道 Catalina 的核心组件包括 链接器和容器,那链接器是干吗的呢?前面讲过,总的来讲就是等待用户的请求,接收用户请求,解析请求,封装成请求对象和响应对象,而后将请求和响应对象交给容器处理。apache

整个过程的细节包括了建立ServerSocket对象,建立处理器池,在每一个处理器内建立请求和响应对象,而后启动全部处理器线程(阻塞,由于没有请求到达,没法处理),而后等待用户请求(阻塞), 用户请求到来时,获取其中一个处理器线程,将socket交给处理器处理,以唤醒处理器线程(利用available变量),让处理器线程解析请求,具体方法是解析端口,解析请求头等信息,解析完后交给容器真正处理请求的内容(如servlet,就是调用咱们常说的invoke方法),处理完后就讲处理器的全部变量恢复到未被使用前,最后回收回线程池。安全

connector.getContainer().invoke(request, response);

Tomcat 容器概述

前面几节咱们都在学习 Tomcat链接器是如何实现的,也学到了它实现的巧妙之处,而这一节咱们要开始学习Tomcat的另外一个组件:容器。和链接器同样,容器有着不可忽略的做用,咱们看到链接器从头至尾都是在作一些基础的工做,从等待请求到解析请求,却没有参与处处理用户真正想要的东西(如servlet要进行某些操做),因此说容器是链接器基础工做作完后,作真正的请求处理操做(加载servlet)。看到咱们上面的链接器总结没,invoke方法就是本节咱们要讲解的重中之重----获取容器执行invoke方法。服务器

容器是一个处理用户 servlet 请求并返回对象给 web 用户的模块。org.apache.catalina.Container 接口定义了容器的形式,有四种容器: Engine(引擎) , Host(主机) , Context(上下文) , 和 Wrapper(包装器)。app

我也按照书上的逻辑,先介绍Context和Wrapper,其它两个留之后讲解。eclipse

对于 Catalina 的容器首先须要注意的是它一共有四种不一样的容器:

  • Engine:表示整个 Catalina 的 servlet 引擎
  • Host:表示一个拥有数个上下文的虚拟主机
  • Context:表示一个 Web 应用,一个 context 包含一个或多个wrapper
  • Wrapper:表示一个独立的 servlet

它们的标准实现是 StandardEngine,StandardHost, StandardContext, and StandardWrapper,它们都是org.apache.catalina.core 包的一部分,这些类都扩展了抽象类 ContainerBase。

要实现一个容器,首先必需要实现 org.apache.catalina.Container 接口。从上一节的链接器讲解中咱们也看到了ex04.pyrmont.core.SimpleContainer 也实现了Container接口,实现了简单的servlet的功能

public class SimpleContainer implements Container {
    public void invoke(Request request, Response response) throws IOException,
        ServletException {

        //中间省略
    }

}

同时,咱们须要知道的是并非每个功能都须要用到四种容器,下面的程序中会看到。

一个容器能够有一个或多个低层次上的子容器,例如,一个 Context 有一个或多个 wrapper,而 wrapper 做为容器层次中的最底层,不能包含子容器。可使用在 Container 接口中定义的 addChild()方法,wrapper调用会抛出异常。

容器的整个处理流程经过一个叫作Pipeline(流水线)的接口实现,这个接口的功能是放置一些Valve(阀门),这些阀门的功能是执行一些基础功能操做,好比记录日志,记录ip等,像流水线同样,经过阀门上下文接口(ValveContext)调用下一个阀门,最后进入一个基础阀门,实现用户真正的请求。我估计若是以前没学过容器的相关内容是看不懂我再说什么的,第二部分再讲解。

Tomcat 容器核心接口:

Pipeline: 一个 pipeline 包含了改容器要唤醒的全部任务。每个阀门表示了一个特定的任务。一个容器的流水线有一个基本的阀门,可是你能够添加任意你想要添加的阀门。阀门的数目定义为添加的阀门的个数(不包括基本阀门)。

Valve:阀门接口表示一个阀门,该组件负责处理请求。

ValveContext: 用于切换到下一个阀门

Contained:一个阀门能够选择性的实现 org.apache.catalina.Contained 接口。该接口定义了其实现类跟一个容器相关联。包含 getContainer和setContainer 方法

Wrapper:org.apache.catalina.Wrapper 接口表示了一个包装器。一个包装器是表示一个独立 servlet 定义的容器。包装器继承了 Container 接口,而且添加了几个方法。包装器的实现类负责管理其下层 servlet 的生命周期,包括 servlet 的 init,service,和 destroy 方法。

Context:一个 context 在容器中表示一个 web 应用。一个 context 一般含有一个或多个包装器做为其子容器。

但愿你们看到不懂的不要沮丧,经过下面的讲解,你们应该都能明白吧。 这里省略了全部链接器的内容,从invoke方法开始讲起。

第二部分:详细讲解

为了模拟容器的实现,容器,流水线等都是用ex05.pyrmont包下相关的类,如SimpleWrapper等,而不是去查看StandardWrapper源代码

启动类Bootstrap1.java

一样的咱们先接触启动类ex05.pyrmont.startup.Bootstrap1.java,这个类是对单个servlet的处理,因此能够看到只用了wrapper,没有用到context。若是咱们要访问,能够用localhost:8080/ModernServlet

public final class Bootstrap1 {

  public static void main(String[] args) {

    HttpConnector connector = new HttpConnector();
    //定义一个包装器类(容器),实际上connector.getContainer().invoke(request, response);
  //就是调用SimpleWrapper对象的invoke方法 Wrapper wrapper = new SimpleWrapper(); wrapper.setServletClass("ModernServlet"); //定义一个类加载器 Loader loader = new SimpleLoader(); //下面就是两个阀门 Valve valve1 = new HeaderLoggerValve(); Valve valve2 = new ClientIPLoggerValve(); //容器绑定类加载器 wrapper.setLoader(loader); //容器添加阀门 ((Pipeline) wrapper).addValve(valve1); ((Pipeline) wrapper).addValve(valve2); //链接器绑定容器,就是connector.getContainer().invoke时要将请求交给容器处理 connector.setContainer(wrapper); try { //下面两个方法是链接器相关内容,再也不重复 connector.initialize(); connector.start(); // make the application wait until we press a key. System.in.read(); } catch (Exception e) { e.printStackTrace(); } } }

固然,直接看注释是没用的,由于咱们不清楚咱们为何要这样作。不急,下面慢慢讲解。记得不断调回这个类整理逻辑哦

SimpleWrapper.java

这个类算是本节比较重要的类了,大部分的接口均可以在这里看到。 SimpleWrapper是invoke出现的地方。

补充说明在代码后面

下面贴上比较重要的代码和相应注释

public class SimpleWrapper implements Wrapper, Pipeline {

  // the servlet instance
  private Servlet instance = null;//加载到的servlet实例
  private String servletClass;
  private Loader loader;//类加载器
  private String name;
    //从一开始就绑定建立的流水线是该容器
  private SimplePipeline pipeline = new SimplePipeline(this);
  protected Container parent = null;

  public SimpleWrapper() {
    //其实咱们在建立SimpleWrapper对象时就设置流水线的基础阀门,经过setBasic
    pipeline.setBasic(new SimpleWrapperValve());
  }

  public synchronized void addValve(Valve valve) {
    //为流水线添加其余Valve
    pipeline.addValve(valve);
  }
    //这个方法的做用是加载Servlet,获取到Servlet实例
    public Servlet allocate() throws ServletException {
    // Load and initialize our instance if necessary
    if (instance==null) {
      try {
        instance = loadServlet();
      }
      catch (ServletException e) {
        throw e;
      }
      catch (Throwable e) {
        throw new ServletException("Cannot allocate a servlet instance", e);
      }
    }
    return instance;
  }
 //具体加载servlet的方法
  private Servlet loadServlet() throws ServletException {
    if (instance!=null)
      return instance;

    Servlet servlet = null;
    String actualClass = servletClass;
    if (actualClass == null) {
      throw new ServletException("servlet class has not been specified");
    }

    Loader loader = getLoader();//获取到咱们定义的类加载器类SimpleLoader,定义和咱们须要的加载方式
    // Acquire an instance of the class loader to be used
    if (loader==null) {
      throw new ServletException("No loader.");
    }
    ClassLoader classLoader = loader.getClassLoader();

    // Load the specified servlet class from the appropriate class loader
    Class classClass = null;
    try {
      if (classLoader!=null) {
        classClass = classLoader.loadClass(actualClass);//加载servlet
      }
    }
    catch (ClassNotFoundException e) {
      throw new ServletException("Servlet class not found");
    }
    // Instantiate and initialize an instance of the servlet class itself
    try {
      servlet = (Servlet) classClass.newInstance();//建立servlet实例
    }
    catch (Throwable e) {
      throw new ServletException("Failed to instantiate servlet");
    }

    // Call the initialization method of this servlet
    try {
//这里调用了servlet的初始化方法,全部跟咱们之前学过的知识同样,init方法只调用一次
      servlet.init(null);
    }
    catch (Throwable f) {
      throw new ServletException("Failed initialize servlet.");
    }
    return servlet;
  }
//就是调用这个invoke方法
 public void invoke(Request request, Response response)
    throws IOException, ServletException {
    pipeline.invoke(request, response);
  }

这个类里面有几个比较关心的方法:

  • invoke方法:咱们讲解的入口
  • addValve方法:添加阀门,其中基础阀门在建立SimpleWrapper实例时就设置了
  • allocate方法:获取类加载器,获取servlet实例

invoke方面里面的是pipeline.invoke(request, response),前面咱们说到,Pineline的做用是负责处理多个任务,

有的人就认为为何不直接调用咱们的Valve,甚至说不直接去掉Pineline,这里我要说一下,咱们不能简简单单的认为容器就只是处理咱们要作的任务,还有不少任务要作的,好比记录是什么IP操做过的,这些就像咱们程序中用的拦截器同样,须要先通过不少的中间步骤。因此回到Bootstrap1.java中的addValve操做,和setLoader方法,能够理解了吧。固然,addValve要真正理解须要继续看下去

那流水线的invoke是如何实现多个任务按顺序处理执行的呢?

SimplePipeline.java

SimpleWrapper.java中一开始就建立流水线对象,并绑定容器为SimpleWrapper,由于invoke中要用到

pipeline.invoke(request, response);

下面查看部分关键代码

public class SimplePipeline implements Pipeline {

 public void invoke(Request request, Response response)
    throws IOException, ServletException {
    // Invoke the first Valve in this pipeline for this request
    (new SimplePipelineValveContext()).invokeNext(request, response);
  }
//添加阀门
 public void addValve(Valve valve) {
        if (valve instanceof Contained)
          ((Contained) valve).setContainer(this.container);

        synchronized (valves) {
          Valve results[] = new Valve[valves.length +1];
          System.arraycopy(valves, 0, results, 0, valves.length);
          results[valves.length] = valve;
          valves = results;
        }
}
public void setBasic(Valve valve) {
    this.basic = valve;
    ((Contained) valve).setContainer(container);
}
//SimplePipeline的内部类,实现ValveContext接口,负责切换到下一个阀门
protected class SimplePipelineValveContext implements ValveContext {

    protected int stage = 0;

    public String getInfo() {
      return null;
    }

    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.length) {//先调用设置的阀门
        valves[subscript].invoke(request, response, this);
      }
      else if ((subscript == valves.length) && (basic != null)) {
        basic.invoke(request, response, this);//最后才是basic阀门
      }
      else {
        throw new ServletException("No valve");
      }
    }
  } // end of inner class

}

须要注意的是:我专门加setBasic方法做为关键代码贴出来,是为了提醒你们SimplePineline上在SimpleWrapper中已经设置了基本Valve,

咱们看到SimplePipeline的invoke方法里面是建立一个内部类SimplePipelineValveContext对象:这个对象负责流水线上阀门Valve的切换,这个内部类实现了ValveContext接口,ValveContext接口中其中一个方法是invokeNext方法,用来调取下一个Valve,具体的实现也不难

  • invokeNext 方法使用下标(subscript)和级别( stage)记住哪一个阀门被唤醒。当第一次唤醒的时候,下标的值是 0,级的值是 1。第一次,第一个阀门被唤醒,流水线的阀门得到 ValveContext 实例调用它的 invokeNext 方法。这时下标的值是 1 因此下一个阀门被唤醒,而后一步步的进行。

下一小节,咱们先选择其中一个阀门讲解

HeaderLoggerValve.java

这个类实现了Valve接口和Contained接口,咱们知道Valve的接口功能是负责处理请求,因此接口里面有invoke方法

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

HeaderLoggerValve.java是用来输出请求头的内容。

public class HeaderLoggerValve implements Valve, Contained {

  protected Container container;


  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    // Pass this request on to the next valve in our pipeline
    valveContext.invokeNext(request, response);//执行下一个阀门

    System.out.println("Header Logger Valve");
    ServletRequest sreq = request.getRequest();
    if (sreq instanceof HttpServletRequest) {
      HttpServletRequest hreq = (HttpServletRequest) sreq;
      Enumeration headerNames = hreq.getHeaderNames();
      while (headerNames.hasMoreElements()) {
        String headerName = headerNames.nextElement().toString();
        String headerValue = hreq.getHeader(headerName);
        System.out.println(headerName + ":" + headerValue);
      }

    }
    else
      System.out.println("Not an HTTP Request");

    System.out.println("------------------------------------");
  }
}

invoke(Request request, Response response, ValveContext context)方法中须要注意的是第三个参数ValveContext context, 咱们会看到SimplePipeline里面传递了this引用,在某个阀门调用context的invokeNext方法时

valveContext.invokeNext(request, response);

以保证下标(subscript)和级别(stage)的值是正确的(由于是同一个引用),下一次执行的阀门就是valve[1], 也就是ClientIPLoggerValve。

最后调用的是基础阀门

basic.invoke(request, response, this);//最后才是basic阀门

SimpleWrapperValve.java

SimpleWrapperValve.java也是一个阀门,因此也实现了Valve和Contained接口

public class SimpleWrapperValve implements Valve, Contained {

  protected Container container;

  public void invoke(Request request, Response response, ValveContext valveContext)
    throws IOException, ServletException {

    SimpleWrapper wrapper = (SimpleWrapper) getContainer();
    ServletRequest sreq = request.getRequest();
    ServletResponse sres = response.getResponse();
    Servlet servlet = null;
    HttpServletRequest hreq = null;
    //看到没,这里强制转换为HttpServletRequest和HttpServletResponse
    if (sreq instanceof HttpServletRequest)
      hreq = (HttpServletRequest) sreq;
    HttpServletResponse hres = null;
    if (sres instanceof HttpServletResponse)
      hres = (HttpServletResponse) sres;

    // Allocate a servlet instance to process this request
    try {
      //加载咱们想访问的真正的servlet,这个方法上面讲了
      servlet = wrapper.allocate();
      if (hres!=null && hreq!=null) {
        servlet.service(hreq, hres);//这里调用了service
      }
      else {
        servlet.service(sreq, sres);
      }
    }
    catch (ServletException e) {
    }
  }

  public String getInfo() {
    return null;
  }

  public Container getContainer() {
    return container;
  }

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

SimpleWrapperValve做为基础阀门,终于找到咱们梦寐已久的service方法

其中SimpleWrapper中咱们看到了这个容器是如何加载servlet的,加载的时候调用了servlet的init方法,

也就是基础阀门的invoke方法中调用加载初始化(wrapper.allocate())

接下来调用了servlet的service方法,

咱们看到servlet是加载一次的而已,因此咱们能够获得结论:Servlet不是线程安全的(此次再也不是只看结论了哦,咱们是推出来的,实在不信,能够看看Standard*的源代码)

第三部分:小结

讲了这么多,将全部的重要接口都混合在代码里面讲解了,不知道你们认识到多少

我感受 Tomcat容器 理解代码仍是次要的 最重要的是明白 Tomcat 为何要这样进行模块设计,咱们能够好好学习这种思想,为何要分这些接口, 无论读什么,读源代码就是这样,在理解是如何自己是如何实现的基础上,理解设计思想。

Point: 读同一份源代码,一万我的有一万中感悟

相应代码能够在个人github上找到下载,拷贝到eclipse,而后打开对应包的代码便可。

如发现编译错误,多是因为jdk不一样版本对编译的要求不一样致使的,能够无论,供学习研究使用。

相关文章
相关标签/搜索