深入剖析Tomcat(五) 剖析Servlet容器并实现一个简易Context与Wrapper容器

上一章介绍了Tomcat的默认连接器,后续程序都会使用默认连接器。前面有讲过Catalina容器的两大块内容就是连接器Servlet容器。不同于第二章的自定义丐版Servlet容器,这一章就来探讨下Catalina中的真正的Servlet容器究竟长啥样。

四种容器

在Catalina中Servlet容器共有四种:Engine、Host、Context、Wrapper,分别对应不同的概念层次

  • Engine:表示整个Catalina servlet引擎;
  • Host:表示包含一个或多个Context的虚拟主机;
  • Context:表示一个Web应用程序,一个Context一般包含多个Wrapper;
  • Wrapper:表示一个独立的servlet。

上述四种容器分别对应org.apache.catalina包内一个接口,分别是

  • org.apache.catalina.Engine
  • org.apache.catalina.Host
  • org.apache.catalina.Context
  • org.apache.catalina.Wrapper

这四个接口都继承自org.apache.catalina.Container接口,代表它们是一种容器。

能看懂上面四个容器都是干啥的吗? 什么,看不懂? 没关系,我们一步一步来了解它们,本章内容只讨论 Context与Wrapper两种容器,Engine与Host在第十三章会详细讲解。

第四章的Bootstrap启动类中有这么段代码,连接器需要关联一个容器,这个容器只要实现了org.apache.catalina.Container就行,而Tomcat中的四种容器都实现了这个接口,所以这里放哪种容器都可以。

java 复制代码
HttpConnector connector = new HttpConnector();
SimpleContainer container = new SimpleContainer();
connector.setContainer(container);

部署功能性的Catalina并不是必须将所有四种容器都包含在内,本章将会提供两个例子,第一个例子只包含Wrapper容器,第二个例子包含Context与Wrapper两种容器。

org.apache.catalina.core包中有一个基础容器抽象类:ContainerBase,该包下还提供了四种容器的标准实现StandardEngine、StandardHost、StandardContext、StandardWrapper,这四个类都继承了ContainerBase,证明四种容器之间是存在共性的,接下来看看ContainerBase这个类,看看容器们有哪些共性。

容器的共性

ContainerBase的定义如下

java 复制代码
public abstract class ContainerBase implements Container, Lifecycle, Pipeline

它实现了三个接口,Container接口我们已经比较熟悉了,Lifecycle生命周期接口下章详细讲解,Pipeline接口是什么呢?下面会讲

我梳理了一下ContainerBase这个类,排除了本章还没接触到的内容后,剩下有这么几项针对四种容器的共性内容

  • 持有一个Pipeline (管道)对象,该对象使用"责任链"设计模式,针对容器类invoke方法接收到的请求做具体处理。
  • 持有一个父容器对象的引用 Container parent;
  • 持有一个子容器对象的map集合,**HashMap<String,Container> children = new HashMap<>();**key为子容器名字,value为子容器对象。
  • 持有一个载入器,**Loader loader;**载入器的主要功能是给容器提供一个类加载器,用于加载servlet。
  • 持有一个映射器,**Mapper mapper;**映射器的主要功能是提供一种方式,可以让容器知晓要把请求内容交给哪个子容器实例进行下一步处理。

这个Pipeline单独讲一下

Pipeline 可以翻译为管道,管道中按顺序排着一排叫做"阀"的东西,英文名叫Valve,阀的数量可多可少,但是至少得有一个坐镇管道尾部的阀,叫做"基础阀"。

在代码中,Pipeline与Valve都以接口的形式定义

在上一章中我们就已经知道,连接器将http请求封装为request与response对象后,就会调用Servlet容器的invoke方法来进行下一步处理。ContainerBase中定义了Servlet容器的invoke方法的默认逻辑为:

java 复制代码
public void invoke(Request request, Response response) throws IOException, ServletException {
    pipeline.invoke(request, response);
}

Servlet容器接到request与response信息后,直接扔给pipeline去处理了,pipeline中的这个处理方法也叫 invoke。

那pipeline拿到request与response后又是怎么做的呢?它又会交给这些排好队的valve们去处理,valve中的处理方法也是叫做invoke,pipeline中invoke方法的伪代码逻辑如下

java 复制代码
public void invoke(Request request, Response response) {
    // 先按顺序将普通阀的逻辑执行一遍
    for (int i = 0; i < valves.length; i++) {
        valves[i].invoke(request,response);
    }
    // 最后执行基础阀的逻辑
    basicValue.invoke(request,response);
}

基础阀是必须的,它负责实现该容器的主要任务。例如Wrapper容器代表一个servlet,它的基础阀的任务就是调用该Wrapper容器对应的servlet的service方法;Context容器代表一个Web应用程序,该容器包含多个Wrapper容器,它的基础阀的任务就是根据请求找到具体对应的Wrapper容器,去调用该Wrapper容器的invoke方法。

基础阀干了主要的事,那普通阀干的就是一些附加的功能了,比如打印下客户端的ip,打印下请求头等。

上面pipeline中的invoke代码示例,只是个伪代码,方便你理解执行逻辑。实际上Tomcat采用另一种方式来实现"责任链"的逻辑。这用到了另一个接口 org.apache.catalina.ValveContext

顾明思议,ValveContext:阀的上下文。这个接口的主要方法为invokeNext,要干的事就是调用"责任链"中下一个"阀"的invoke方法。这个接口怎么用的呢?来看下StandardPipeline中的部分逻辑

java 复制代码
public class StandardPipeline implements Pipeline, Contained, Lifecycle {

    // 基础阀
    protected Valve basic = null;

    // 关联的容器对象
    protected Container container = null;

    // 此管道拥有的所有阀
    protected Valve[] valves = new Valve[0];

    /**
     * 接手容器收到的Request与Response请求,由与此管道关联的阀挨个处理,直到基础阀处理完成。 
     * 此方法要保证在被并发调用(同时被不同线程调用)情况下不会出现问题。
     */
    public void invoke(Request request, Response response) throws IOException, ServletException {

        // 交给ValveContext来保证阀的调用顺序
        (new StandardPipelineValveContext()).invokeNext(request, response);

    }

    /**
    * 内部类,可以使用外部类的valves属性
    */
    protected class StandardPipelineValveContext implements ValveContext {

        protected int stage = 0;

        /**
         * 调用下一个阀的invoke方法,阀的invoke方法参数之一为当前ValveContext对象
         */
        public void invokeNext(Request request, Response response) throws IOException, ServletException {

            int subscript = stage;
            stage = stage + 1;

            if (subscript < valves.length) {
                valves[subscript].invoke(request, response, this);
            } else if ((subscript == valves.length) && (basic != null)) {
                basic.invoke(request, response, this);
            } else {
                throw new ServletException(sm.getString("standardPipeline.noValve"));
            }
        }

    }

}

普通阀的伪代码

java 复制代码
public class MyValve implements Valve, Contained {

    protected Container container;

    public void invoke(Request request, Response response, ValveContext valveContext)
            throws IOException, ServletException {
        // 处理逻辑...

        // 利用valveContext来调用下一个阀的invoke方法
        valveContext.invokeNext(request, response);
        
        // 处理逻辑...
    }

}

StandardPipeline中拥有一个已经排好顺序的阀的数组 valves ,StandardPipeline的invoke方法的实现为:实例化一个StandardPipelineValveContext对象,并调用其invokeNext方法。StandardPipelineValveContext是StandardPipeline的一个非静态内部类,它可以使用StandardPipeline中的属性,主要是为了使用 valves 属性,StandardPipelineValveContext中有一个属性stage,代表当前"责任链"已经执行到哪个阀了,invokeNext方法的作用则为调用"责任链"中下一个阀的invoke方法。总的来说,StandardPipelineValveContext的作用就是根据管道中已经排好的阀们,使用invokeNext方法调用下一个阀的invoke方法,直到基础阀执行完。

阀的invoke方法中的第三个参数则为StandardPipelineValveContext对象,每个非基础阀的invoke方法中都需要有这段代码 **valveContext.invokeNext(request, response);**以确保责任链能继续执行下去,而基础阀中是不应该有这段代码的,"责任链"到基础阀就该结束了。

另外StandardPipeline和MyValve还实现了org.apache.catalina.Contained接口。谁实现了这个接口,就代表谁要持有一个容器对象的引用,容器的处理逻辑相关类一般会实现这个接口。

Pipeline的介绍到此为止,我们继续回到四种容器的共性研究上。上面说了,每种容器都有 管道、父容器、子容器、加载器、映射器等等其他一些本章没描述的东西。长的都还有点像,这种结构像什么呢?

没错,俄罗斯套娃,哈哈!稍微不同的是,在Tomcat中,父容器一下子可以套多个子容器。

Tomcat中的容器相似点就介绍到这里,Engine与Host容器会编排到第十三章来讲,中间还需要做些铺垫。本章主要介绍Context与Wrapper两种容器。

Wrapper容器

Wrapper容器是最底层的容器,它没有子容器了。Wrapper容器是对一个具体Servlet的封装,负责Servlet整个生命周期,在合适的时机会调用Servlet的 init、service、destroy 方法。

Wrapper接口中比较重要的方法为load() 和**allocate()**方法。Wrapper容器中拥有一个指定servlet的全限定名servletClass,load方法负责加载并初始化一个servlet实例,allocate方法负责提供一个可用的servlet实例(即可以执行service方法的servlet实例)。

接下来,我们来编写一个只有Wrapper容器的Servlet容器,本次设计相关的UML图如下

SimpleWrapper代表一个Wrapper容器,负责维护一个servlet。

SimplePipeline代表一个容器管道,可以被SimpleWrapper持有。

ClientIPLoggerValve、HeaderLoggerValve是普通阀,他们可被编排在SimplePipeline中。

SimpleWrapperVave是针对Wrapper容器的基础阀,负责加载servlet并调用其service方法。

SimpleLoader代表一个加载器,在本章代码中主要负责提供一个类加载器。

这些个类共同组成一个最小的servlet容器

SimpleLoader类

该类负责向外提供一个类加载器,负责加载serlvet。

由于Loader接口的很多方法暂时没实现,所以留空的方法不在展示出来,下面是精简后的代码

java 复制代码
public class SimpleLoader implements Loader {

    // servlet类所在目录
    public static final String WEB_ROOT = System.getProperty("user.dir") + File.separator + "webroot";

    // 类加载器
    ClassLoader classLoader = null;

    // 容器
    Container container = null;

    // SimpleLoader在构造时就创建了一个类加载器负责加载servlet
    public SimpleLoader() {
        try {
            URL[] urls = new URL[1];
            URLStreamHandler streamHandler = null;
            File classPath = new File(WEB_ROOT);
            String repository = (new URL("file", null, classPath.getCanonicalPath() + File.separator)).toString();
            urls[0] = new URL(null, repository, streamHandler);
            classLoader = new URLClassLoader(urls);
        } catch (IOException e) {
            System.out.println(e.toString());
        }
    }

    public ClassLoader getClassLoader() {
        return classLoader;
    }

}

SimplePipeline类

该类是一个简易通道类,含有一个内部类SimplePipelineValveContext来负责阀们"责任链"的编排。

java 复制代码
package ex05.pyrmont.core;

import org.apache.catalina.*;

import javax.servlet.ServletException;
import java.io.IOException;

public class SimplePipeline implements Pipeline {

    public SimplePipeline(Container container) {
        setContainer(container);
    }

    // 基础阀
    protected Valve basic = null;
    // 关联的容器
    protected Container container = null;
    // 普通阀数组
    protected Valve valves[] = new Valve[0];

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

    public Valve getBasic() {
        return basic;
    }

    public void setBasic(Valve valve) {
        this.basic = valve;
        ((Contained) valve).setContainer(container);
    }

    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 Valve[] getValves() {
        return valves;
    }

    public void invoke(Request request, Response response) throws IOException, ServletException {
        // 调用该管道中第一个阀的invoke方法
        (new SimplePipelineValveContext()).invokeNext(request, response);
    }

    public void removeValve(Valve valve) {
    }

    // 内部类,负责阀的"责任链"的执行编排
    protected class SimplePipelineValveContext implements ValveContext {

        // 记录当前执行到哪一个阀了
        protected int stage = 0;

        public String getInfo() {
            return null;
        }
        
        // 执行下一个阀的invoke方法
        public void invokeNext(Request request, Response response)
                throws IOException, ServletException {
            int subscript = stage;
            stage = stage + 1;
            if (subscript < valves.length) {
                valves[subscript].invoke(request, response, this);
            } else if ((subscript == valves.length) && (basic != null)) {
                basic.invoke(request, response, this);
            } else {
                throw new ServletException("No valve");
            }
        }
    } // end of inner class

}

SimpleWrapper类

该类代表一个Wrapper容器,实现了org.apache.catalina.Wrapper接口,并提供了load()和allocate()方法的实现。

SimpleWrapper拥有一个servletClass属性,代表其对应的servlet的全限定名;还设置了一个instance属性,代表serlvet的实例化对象;拥有一个载入器对象loader,负责提供类加载器;拥有一个pipeline管道;拥有一个parent属性,代表父容器,SimpleWrapper可以作为Context容器的子容器。

SimpleWrapper在构造方法中就指定了SimpleWrapperValve作为其管道中的基础阀。

Wrapper接口的很多方法都没实现,所以不再展示留空的方法,下面是精简后的代码

java 复制代码
public class SimpleWrapper implements Wrapper, Pipeline {

    // servlet的实例
    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() {
        pipeline.setBasic(new SimpleWrapperValve());
    }

    public synchronized void addValve(Valve valve) {
        pipeline.addValve(valve);
    }

    public Servlet allocate() throws ServletException {
        // 如果需要的话,加载并实例化一个servlet对象
        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;
    }

    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();
        if (loader == null) {
            throw new ServletException("No loader.");
        }
        ClassLoader classLoader = loader.getClassLoader();

        // 使用载入器提供的类加载器,加载特定的servlet类
        Class classClass = null;
        try {
            if (classLoader != null) {
                classClass = classLoader.loadClass(actualClass);
            }
        } catch (ClassNotFoundException e) {
            throw new ServletException("Servlet class not found");
        }
        // 创建一个servlet对象
        try {
            servlet = (Servlet) classClass.newInstance();
        } catch (Throwable e) {
            throw new ServletException("Failed to instantiate servlet");
        }

        // 调用servlet的init方法
        try {
            servlet.init(null);
        } catch (Throwable f) {
            throw new ServletException("Failed initialize servlet.");
        }
        return servlet;
    }

    /**
     * 获取加载器,如果此容器加载器为空的话则去获取父容器的
     */
    public Loader getLoader() {
        if (loader != null)
            return (loader);
        if (parent != null)
            return (parent.getLoader());
        return (null);
    }

    public void setLoader(Loader loader) {
        this.loader = loader;
    }

    public void setServletClass(String servletClass) {
        this.servletClass = servletClass;
    }

    public void invoke(Request request, Response response)
            throws IOException, ServletException {
        pipeline.invoke(request, response);
    }

    public void load() throws ServletException {
        instance = loadServlet();
    }

}

SimpleWrapperValve类

该类是SimpleWrapper对应的基础阀,专门用于处理对SimpleWrapper类的请求,加载、实例化servlet并执行其service方法。

基础阀的invoke方法中不需要也不应该调用传递给它的ValveContext示例的invokeNext方法。

java 复制代码
package ex05.pyrmont.core;

import org.apache.catalina.*;

import javax.servlet.Servlet;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


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;
        if (sreq instanceof HttpServletRequest) {
            hreq = (HttpServletRequest) sreq;
        }
        HttpServletResponse hres = null;
        if (sres instanceof HttpServletResponse) {
            hres = (HttpServletResponse) sres;
        }

        // 获取一个servlet实例用以处理请求
        try {
            servlet = wrapper.allocate();
            if (hres != null && hreq != null) {
                servlet.service(hreq, hres);
            } 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;
    }
}

ClientIPLoggerValve与HeaderLoggerValve

这是两个普通阀,都实现了org.apache.catalina.valve接口,ClientIPLoggerValve用于将客户端的IP地址输出到控制台上,HeaderLoggerValve会把请求头信息输出到控制台上。

Valve的invoke方法定义为

void invoke(Request request, Response response,ValveContext context)

普通阀的invoke方法中,需要调用ValveContext参数对象的invokeNext方法,来执行管道中下一个阀的invoke方法。

java 复制代码
public class ClientIPLoggerValve 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("Client IP Logger Valve");
        ServletRequest sreq = request.getRequest();
        System.out.println(sreq.getRemoteAddr());
        System.out.println("------------------------------------");
    }

    public String getInfo() {
        return null;
    }

    public Container getContainer() {
        return container;
    }

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

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

    public String getInfo() {
        return null;
    }

    public Container getContainer() {
        return container;
    }

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

Bootstrap1类

这是一个启动类,负责利用上面的容器相关类,组织成一个容器,连接器的话还是使用Tomcat的默认连接器,main方法运行起来后就可以接收请求了。

由于这个容器中仅有一个Wrapper容器,关联了ModernServlet,所以无论客户端什么请求都会打到ModernServlet中。本章的下个程序将会包含多个Wrapper,届时会根据不同uri请求不同srevlet。

java 复制代码
package ex05.pyrmont.startup;

import ex05.pyrmont.core.SimpleLoader;
import ex05.pyrmont.core.SimpleWrapper;
import ex05.pyrmont.valves.ClientIPLoggerValve;
import ex05.pyrmont.valves.HeaderLoggerValve;
import org.apache.catalina.Loader;
import org.apache.catalina.Pipeline;
import org.apache.catalina.Valve;
import org.apache.catalina.Wrapper;
import org.apache.catalina.connector.http.HttpConnector;

public final class Bootstrap1 {
    public static void main(String[] args) {

        // 使用Tomcat的默认连接器
        HttpConnector connector = new HttpConnector();
        
        // 构建Wrapper容器
        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);

        // 将wrapper容器与连接器做个关联
        connector.setContainer(wrapper);

        try {
            connector.initialize();
            connector.start();

            // 保活main线程,直到键盘输入了任何字符
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果展示

Bootstrap1启动后,浏览器调用结果

Context容器

一个Context容器其实就可以代表一个完整的应用了。一个Context实例可以包含一个或多个Wrapper实例作为子容器。

通常情况下一个Web应用的serlvet是有多个的,也就是存在多个Wrapper实例,Context容器接收到连接器给的请求对象后,如何知道该将这个请求交给哪个Wrapper来处理呢?这就需要一个映射器了。

开篇介绍容器的共性时就提到了,容器会有一个映射器,他会提供一种方式,让容器知晓它该把请求交给哪个子容器实例来进行下一步的处理。在Catalina中映射器的定义是一个接口:org.apache.catalina.Mapper

getContainner()、setContainer() 两个方法说明映射器得持有一个容器实例的引用。

getProtocol()、setProtocol() 两个方法说明映射器是针对协议进行区分的,一个映射器实例仅处理一种协议的请求,比如http协议或者https协议等。

map()方法是实现映射器主要逻辑的方法,它负责根据Request映射出一个子容器实例,然后父容器就将请求信息给到该实例,由该实例进行进一步处理。

接下来看下本章第二个程序(Context应用程序)的设计UML图

相比于第一个程序,该程序添加了如下几个类

SimpleContext:一个简易的Context容器,持有Mapper映射器和多个Wrapper实例

SimpleContextMapper:Context的映射器

SimpleContextValve:Context容器的基础阀,主要逻辑为将特定请求给到特定的Wapper实例。

这几个类加第一个程序的类,就组成了一个以Context为顶层容器的servlet容器。

Context应用程序使用了与第一个应用程序相同的载入器和两个阀,但是,载入器与阀都是与Context容器(而非Wapper)相关联的。这样,两个Wrapper就都可以使用这个载入器。Context实例作为servlet容器被设置到连接器中。这样,当连接器收到http请求后,就可以调用Context的invoke方法了。

Context应用程序中Wrapper容器管道中就只放了一个基础阀SimpleWrapperValve,没有其他阀了。

Context与Wrapper的两个管道要区分好了,连接器会先将请求给到Context,Context将请求给到Context管道,请求经过几个普通阀的处理后来到了Context基础阀,Context基础阀挑出来一个Wrapper实例,将请求交给该Wrapper,Wrapper又将请求给到Wrapper管道,Wrapper基础阀负责调用具体servlet的service方法。

来看看具体的代码

SimpleContextValve类

Context容器的基础阀,负责将请求交给特定的Wrapper,主要逻辑在invoke方法中,其中获取特定Wrapper的逻辑其实是通过调用SimpleContext类中的map方法来实现的。

java 复制代码
package ex05.pyrmont.core;

import org.apache.catalina.*;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;


public class SimpleContextValve implements Valve, Contained {

    protected Container container;

    public void invoke(Request request, Response response, ValveContext valveContext) throws IOException, ServletException {
        // 验证request和response对象的类型
        if (!(request.getRequest() instanceof HttpServletRequest) || !(response.getResponse() instanceof HttpServletResponse)) {
            return;
        }

        String requestURI = ((HttpRequest) request).getDecodedRequestURI();

        Context context = (Context) getContainer();
        // 挑选一个 Wrapper 处理该请求
        Wrapper wrapper;
        try {
            wrapper = (Wrapper) context.map(request, true);
        } catch (IllegalArgumentException e) {
            badRequest(requestURI, (HttpServletResponse) response.getResponse());
            return;
        }
        if (wrapper == null) {
            notFound(requestURI, (HttpServletResponse) response.getResponse());
            return;
        }
        // 调用 Wrapper 的invoke方法处理请求
        response.setContext(context);
        wrapper.invoke(request, response);
    }

    public String getInfo() {
        return null;
    }

    public Container getContainer() {
        return container;
    }

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

    private void badRequest(String requestURI, HttpServletResponse response) {
        try {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST, requestURI);
        } catch (Exception e) {
            ;
        }
    }

    private void notFound(String requestURI, HttpServletResponse response) {
        try {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, requestURI);
        } catch (Exception e) {
            ;
        }
    }

}

SimpleContextMapper类

该类实现了org.apache.catalina.Mapper接口,代表它是一个映射器。它持有一个SimpleContext对象的引用,它的主要逻辑在map方法中,负责根据特定请求找到一个特定的Wrapper。map方法有两个参数,一个Request对象和一个布尔变量。本应用程序中忽略第二个参数。

java 复制代码
package ex05.pyrmont.core;

import org.apache.catalina.*;

import javax.servlet.http.HttpServletRequest;

public class SimpleContextMapper implements Mapper {

    // 映射器关联的Context容器对象
    private SimpleContext context = null;

    public Container getContainer() {
        return (context);
    }

    public void setContainer(Container container) {
        if (!(container instanceof SimpleContext)) {
            throw new IllegalArgumentException("Illegal type of container");
        }
        context = (SimpleContext) container;
    }
    
    /**
     * 根据请求的特征返回子容器,该子容器用于处理此请求。
     * 如果没有这样的子容器可以被识别,则返回null
     */
    public Container map(Request request, boolean update) {
        // 获取相对路径的uri作为映射的key
        String contextPath = ((HttpServletRequest) request.getRequest()).getContextPath();
        String requestURI = ((HttpRequest) request).getDecodedRequestURI();
        String relativeURI = requestURI.substring(contextPath.length());
        // 调用Context中相关方法拿到特定Wrapper
        Wrapper wrapper = null;
        String servletPath = relativeURI;
        String name = context.findServletMapping(servletPath);
        if (name != null) {
            wrapper = (Wrapper) context.findChild(name);
        }
        return (wrapper);
    }

    public String getProtocol() {
        return null;
    }

    public void setProtocol(String protocol) {
    }
    
}

SimpleContext类

Context应用程序的Context容器就是SimpleContext类的实例。SimpleContext类实现了Context与Pipeline接口,但是接口的大部分方法都暂未实现,但与映射相关的方法都给出了具体的实现。这些方法包括以下几个

addMapper():在Context容器中添加一个映射器。SimpleContext类有声明两个变量:mapper与mappers。mappers代表Context容器拥有的所有映射器,针对不同协议用不同映射器。如果mappers中只有一个映射器的话,则将该映射器赋给mapper对象,代表默认映射器;反之,如果mappers中有多个映射器的话,则mapper属性置为null。这个设定在findMapper方法中会用到。

java 复制代码
protected Mapper mapper = null;
protected HashMap<String,Mapper> mappers = new HashMap<>();


public void addMapper(Mapper mapper) {
    synchronized (mappers) {
        // 如果相同协议的映射器已经被添加过了,则抛出异常
        if (mappers.get(mapper.getProtocol()) != null) {
            throw new IllegalArgumentException("addMapper:  Protocol '" + mapper.getProtocol() + "' is not unique");
        }
        mappers.put(mapper.getProtocol(), mapper);
        mapper.setContainer(this);      // May throw IAE
        if (mappers.size() == 1) {
            this.mapper = mapper;
        } else {
            this.mapper = null;
        }
    }
}

fiindMapper():从Context容器中获取映射器,如果默认映射器有值的话则返回默认映射器,否则根据指定协议返回指定的映射器。

java 复制代码
public Mapper findMapper(String protocol) {
    if (mapper != null) {
        return mapper;
    } else synchronized (mappers) {
        return mappers.get(protocol);
    }
}

addServletMapping():这方法有两个参数,(String pattern, String name)。pattern代表请求uri的一个路径,name代表Wrapper容器的名字。要知道SimpleContext中对于子容器的定义是这样的

HashMap<String, Container> children = new HashMap<>();

children的key就是Wrapper实例的名字,value就是Wrapper实例。 而addServletMapping方法的name参数也是Wrapper实例的名字,所以,通过name就可以找到Wrapper实例。

java 复制代码
protected HashMap<String, Container> children = new HashMap<>();
protected final HashMap<String,String> servletMappings = new HashMap<>();

public void addServletMapping(String pattern, String name) {
    synchronized (servletMappings) {
        servletMappings.put(pattern, name);
    }
}

**findServletMapping():**通过uri路径找到对应的Wrapper容器的name

java 复制代码
public String findServletMapping(String pattern) {
    synchronized (servletMappings) {
        return servletMappings.get(pattern);
    }
}

map():通过request请求,找到一个能处理该请求的特定Wrapper

java 复制代码
public Container map(Request request, boolean update) {
    // 本次代码中,findMapper方法永远只会返回默认映射器
    Mapper mapper = findMapper(request.getRequest().getProtocol());
    if (mapper == null) {
        return null;
    }

    // 调用映射器的map方法,找到特定的Wrapper容器
    return mapper.map(request, update);
}

通过上面几个方法,可以了解到SimpleContext容器:

  • 持有一个子容器实例的Map ( key: Wrapper实例name, value:Wrapper实例对象)。
  • 持有一个uri与Wrapper实例name的映射关系Map (key:uri处理后的路径, value:Wrapper实例name)。
  • 持有一个映射器集合 HashMap<String,Mapper> mappers;

基于以上几点,再看Context容器基础阀SimpleContextValve#invoke方法的流程

html 复制代码
1. 根据请求对象request,调用SimpleContext的map方法去获取Wrapper对象         
  1.1 SimpleContext#map方法通过request请求的对应协议,找到一个匹配的映射器
  1.2 调用映射器Mapper#map方法通过request请求,去获取Wrapper对象
    1.2.1 Mapper#map方法根据request请求的uri,调用SimpleContext#findServletMapping方法找到该请求对应的Wrapper实例的name
    1.2.2 调用SimpleContext#findChild方法,通过Wrapper实例的name,找到对应的Wrapper实例对象
2. Wrapper实例对象调用其invoke方法执行接下来的逻辑,接下来的逻辑就与第一个应用程序的逻辑一样了

SimpleContext类的代码如下,我精简了一下代码,去掉了一些没有实现的接口方法

java 复制代码
public class SimpleContext implements Context, Pipeline {

    public SimpleContext() {
        pipeline.setBasic(new SimpleContextValve());
    }

    private Container parent = null;
    protected HashMap<String, Container> children = new HashMap<>();
    protected Loader loader = null;
    protected SimplePipeline pipeline = new SimplePipeline(this);
    protected final HashMap<String,String> servletMappings = new HashMap<>();
    protected Mapper mapper = null;
    protected HashMap<String,Mapper> mappers = new HashMap<>();


    public void invoke(Request request, Response response) throws IOException, ServletException {
        pipeline.invoke(request, response);
    }

    public void addServletMapping(String pattern, String name) {
        synchronized (servletMappings) {
            servletMappings.put(pattern, name);
        }
    }

    public String findServletMapping(String pattern) {
        synchronized (servletMappings) {
            return servletMappings.get(pattern);
        }
    }

    public Loader getLoader() {
        if (loader != null) return (loader);
        if (parent != null) return (parent.getLoader());
        return (null);
    }

    public void addChild(Container child) {
        child.setParent((Container) this);
        children.put(child.getName(), child);
    }

    /**
     * 向Context容器中添加映射器,第一个被加进来的映射器将作为默认映射器
     */
    public void addMapper(Mapper mapper) {
        synchronized (mappers) {
            // 如果相同协议的映射器已经被添加过了,则抛出异常
            if (mappers.get(mapper.getProtocol()) != null) {
                throw new IllegalArgumentException("addMapper:  Protocol '" + mapper.getProtocol() + "' is not unique");
            }
            mappers.put(mapper.getProtocol(), mapper);
            mapper.setContainer(this);      // May throw IAE
            if (mappers.size() == 1) {
                this.mapper = mapper;
            } else {
                this.mapper = null;
            }
        }
    }

    public Container findChild(String name) {
        if (name == null) {
            return null;
        }
        synchronized (children) {
            return children.get(name);
        }
    }

    /**
     * 如果默认映射器有值的话,则该方法使用返回默认映射器,无论是什么协议(protocol)
     */
    public Mapper findMapper(String protocol) {
        if (mapper != null) {
            return mapper;
        } else synchronized (mappers) {
            return mappers.get(protocol);
        }
    }

    /**
     * 通过request请求找到特定的Wrapper实例
     */
    public Container map(Request request, boolean update) {
        // 本次代码中,findMapper方法永远只会返回默认映射器
        Mapper mapper = findMapper(request.getRequest().getProtocol());
        if (mapper == null) {
            return null;
        }

        // 调用映射器的map方法,找到特定的Wrapper容器
        return mapper.map(request, update);
    }

}

Bootstrap2类

该类是Context应用程序的启动类。

本次应用程序包含两个servlet:PrimitiveServlet与ModernServlet,两个servlet对应两个Wrapper实例,对应的名字为Primitive与Modern,它们作为子容器被包含在Context容器中。

本次应用程序只有一个映射器可以使用,所以这个映射器也作为Context容器的默认映射器。映射器指定了uri与Wrapper容器name的映射关系为:

  • /Primitive → Primitive
  • /Modern → Modern

连接器仍然使用Tomcat的默认连接器

java 复制代码
package ex05.pyrmont.startup;

import ex05.pyrmont.core.SimpleContext;
import ex05.pyrmont.core.SimpleContextMapper;
import ex05.pyrmont.core.SimpleLoader;
import ex05.pyrmont.core.SimpleWrapper;
import ex05.pyrmont.valves.ClientIPLoggerValve;
import ex05.pyrmont.valves.HeaderLoggerValve;
import org.apache.catalina.*;
import org.apache.catalina.connector.http.HttpConnector;

public final class Bootstrap2 {
    public static void main(String[] args) {
        HttpConnector connector = new HttpConnector();
        Wrapper wrapper1 = new SimpleWrapper();
        wrapper1.setName("Primitive");
        wrapper1.setServletClass("PrimitiveServlet");
        Wrapper wrapper2 = new SimpleWrapper();
        wrapper2.setName("Modern");
        wrapper2.setServletClass("ModernServlet");

        Context context = new SimpleContext();
        context.addChild(wrapper1);
        context.addChild(wrapper2);

        Valve valve1 = new HeaderLoggerValve();
        Valve valve2 = new ClientIPLoggerValve();

        ((Pipeline) context).addValve(valve1);
        ((Pipeline) context).addValve(valve2);

        Mapper mapper = new SimpleContextMapper();
        mapper.setProtocol("http");
        context.addMapper(mapper);
        Loader loader = new SimpleLoader();
        context.setLoader(loader);
        // context.addServletMapping(pattern, name);
        context.addServletMapping("/Primitive", "Primitive");
        context.addServletMapping("/Modern", "Modern");
        connector.setContainer(context);
        try {
            connector.initialize();
            connector.start();

            // make the application wait until we press a key.
            System.in.read();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果展示

Bootstrap2启动后,浏览器访问结果展示

/Primitive

/Modern

后台日志

bash 复制代码
HttpConnector Opening server socket on all host IP addresses
HttpConnector[8080] Starting background thread
PrimitiveServlet -- init
Client IP Logger Valve
127.0.0.1
------------------------------------
Header Logger Valve
host:127.0.0.1:8080
connection:keep-alive
sec-ch-ua:"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile:?0
sec-ch-ua-platform:"macOS"
upgrade-insecure-requests:1
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site:none
sec-fetch-mode:navigate
sec-fetch-user:?1
sec-fetch-dest:document
accept-encoding:gzip, deflate, br, zstd
accept-language:zh-CN,zh;q=0.9
------------------------------------
ModernServlet -- init
Client IP Logger Valve
127.0.0.1
------------------------------------
Header Logger Valve
host:127.0.0.1:8080
connection:keep-alive
sec-ch-ua:"Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"
sec-ch-ua-mobile:?0
sec-ch-ua-platform:"macOS"
upgrade-insecure-requests:1
user-agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36
accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
sec-fetch-site:none
sec-fetch-mode:navigate
sec-fetch-user:?1
sec-fetch-dest:document
accept-encoding:gzip, deflate, br, zstd
accept-language:zh-CN,zh;q=0.9
------------------------------------

好,本章内容就到这里,在这一章里,我们动手实现了一个仅有Wrapper的servlet容器,又实现了一个既有Context又有Wrapper的servlet容器;Pipeline的原理使我们对责任链机制又储备了一个实际参考案例;由于容器实例的单例性,要保证并发请求时不会出现问题,主要是保证对象的实例属性不轻易变化;Engine与Host容器在做足铺垫后将在第13章详细介绍。下一章我们一起来看看Tomcat中生命周期相关的内容,敬请期待!

源码分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

本章代码基于原书中代码做了些许美化,添加了注释,代码没有重大bug就不再另起一个包去做实现了,本章代码在这个包下

相关推荐
考虑考虑1 天前
Jpa使用union all
java·spring boot·后端
用户3721574261351 天前
Java 实现 Excel 与 TXT 文本高效互转
java
浮游本尊1 天前
Java学习第22天 - 云原生与容器化
java
渣哥1 天前
原来 Java 里线程安全集合有这么多种
java
间彧1 天前
Spring Boot集成Spring Security完整指南
java
间彧1 天前
Spring Secutiy基本原理及工作流程
java
Java水解1 天前
JAVA经典面试题附答案(持续更新版)
java·后端·面试
洛小豆1 天前
在Java中,Integer.parseInt和Integer.valueOf有什么区别
java·后端·面试
前端小张同学1 天前
服务器上如何搭建jenkins 服务CI/CD😎😎
java·后端
ytadpole1 天前
Spring Cloud Gateway:一次不规范 URL 引发的路由转发404问题排查
java·后端