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

news2025/1/11 12:44:32

上一章介绍了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中的四种容器都实现了这个接口,所以这里放哪种容器都可以。

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的定义如下

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方法的默认逻辑为:

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方法的伪代码逻辑如下

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中的部分逻辑

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

    }

}

普通阀的伪代码

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接口的很多方法暂时没实现,所以留空的方法不在展示出来,下面是精简后的代码

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来负责阀们“责任链”的编排。

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接口的很多方法都没实现,所以不再展示留空的方法,下面是精简后的代码

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方法。

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方法。

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。

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方法来实现的。

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对象和一个布尔变量。本应用程序中忽略第二个参数。

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方法中会用到。

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容器中获取映射器,如果默认映射器有值的话则返回默认映射器,否则根据指定协议返回指定的映射器。

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实例。

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

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

map():通过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);
}

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

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

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

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类的代码如下,我精简了一下代码,去掉了一些没有实现的接口方法

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的默认连接器

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

后台日志

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就不再另起一个包去做实现了,本章代码在这个包下

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1635915.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VTK —— 二、教程六 - 为模型加入3D微件(按下i键隐藏或显示)(附完整源码)

代码效果 本代码编译运行均在如下链接文章生成的库执行成功&#xff0c;若无VTK库则请先参考如下链接编译vtk源码&#xff1a; VTK —— 一、Windows10下编译VTK源码&#xff0c;并用Vs2017代码测试&#xff08;附编译流程、附编译好的库、vtk测试源码&#xff09; 教程描述 本…

数据挖掘之基于Lightgbm等多模型消融实验的信用欺诈检测实现

欢迎大家点赞、收藏、关注、评论啦 &#xff0c;由于篇幅有限&#xff0c;只展示了部分核心代码。 文章目录 一项目简介 二、功能三、系统四. 总结 一项目简介 一、项目背景 在当前的金融环境中&#xff0c;信用欺诈行为日益增多&#xff0c;给金融机构和消费者带来了巨大的损…

RS0108YQ20功能和参数介绍及高速数据传输中的优势

RS0108YQ20功能和参数介绍及高速数据传输中的优势-公司新闻-配芯易-深圳市亚泰盈科电子有限公司 RS0108YQ20是一款电平转换器&#xff0c;也称为电平移位器&#xff0c;它的主要功能是在不同的电源电压或逻辑电平之间提供双向信号转换。以下是RS0108YQ20的一些关键参数和功能特…

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例

使用UmcFramework和unimrcpclient.xml连接多个SIP设置的配置指南及C代码示例 引言1. UniMRCP和UmcFramework简介2. 准备工作3. unimrcpclient.xml配置文件3.1 定义SIP设置3.2 定义MRCP会话配置文件 4. C代码示例5. 测试和验证6. 故障排查7. 结论8. 参考文献 引言 在多媒体通信…

Django后台项目开发实战二

我们的需求是开发职位管理系统 三个功能&#xff1a; 管理员发布职位候选人能浏览职位用户能投递职位 第二阶段 创建应用 jobs&#xff0c;实现职位数据的建模 python manage.py startapp jobs 然后再 setting .py 注册应用&#xff0c;只需添加应用名称到最后一行 INST…

单片机排队叫号系统Proteus仿真程序 有取号键和叫号键以及重复叫号键 有注释

目录 1、前言 ​ 2、程序 资料下载地址&#xff1a;单片机排队叫号系统Proteus仿真程序 有取号键和叫号键以及重复叫号键 有注释 1、前言 系统组成&#xff1a;STC89C52RCLcd1602蜂鸣器按键 具体介绍&#xff1a; Lcd1602排队叫号系统&#xff0c;有取号显示窗和叫号显示窗…

Ubuntu 16.04下Firefox版本更新

最近要使用Odoo进行项目管理&#xff0c;Odoo17以上版本对浏览器版本要求较高&#xff0c;如果没有新版本下的函数&#xff0c;将无法运行。而Ubuntu16.04下自带的firefox不满足版本要求&#xff0c;因而需要手动下载安装。 查看当前系统版本apt-get能下载的firefox版本 apt-c…

排序算法大总结

引言 排序算法&#xff08;sorting algorithm&#xff09;是用于对一组数据按照特定顺序进行排列。排序算法有着广泛的应用&#xff0c;因为有序数据通常能够被更高效地查找、分析和处理。 如图 1-1 所示&#xff0c;排序算法中的数据类型可以是整数、浮点数、字符或字符串等…

ubuntu下anaconda虚拟环境开机自启动

&#xff08;1&#xff09; 要在Ubuntu系统中使Anaconda环境下的Python脚本在开机时自启动&#xff0c;可以通过创建一个systemd服务单元来实现。以下是步骤和示例代码&#xff1a; 创建一个新的systemd服务文件。 打开文本编辑器&#xff0c;创建一个新的服务文件。例如&…

idea生成双击可执行jar包

我这里是一个生成xmind,解析sql的一个main方法,可以通过配置文件来修改有哪些类会执行 我们经常会写一个处理文件的main方法,使用时再去寻找,入入会比较麻烦,这里就可以把我们写过的main方法打成jar包,放到指定的目录来处理文件并生成想要的结果 1.写出我们自己的main方法,本地…

【Java笔记】JVM:对象在内存中是什么样的?如何计算对象占用的内存大小?

文章目录 Java对象的内存布局计算对象占用的内存大小Openjdk jol来算几个Object o new Object() 该对象在内存中占用多少字节&#xff1f;基本数据类型作为成员变量的对象有实例对象作为成员变量的对象 Java对象的内存布局 Java中&#xff0c;一个实例对象在内存中的组成主要包…

(40)4.30数据结构(队列)

1.队列的基本概念 2.队列的顺序 #define MaxSize 10 #define ElemType int typedef struct { ElemType data[MaxSize]; int front, rear; }SqQueue;//1.初始化操作 void InitQueue(SqQueue& Q) { //初始化 队头&#xff0c;队尾指针指向0 Q.rear Q.fron…

大数据分析与内存计算学习笔记

一、Scala编程初级实践 1.计算级数&#xff1a; 请用脚本的方式编程计算并输出下列级数的前n项之和Sn&#xff0c;直到Sn刚好大于或等于q为止&#xff0c;其中q为大于0的整数&#xff0c;其值通过键盘输入。&#xff08;不使用脚本执行方式可写Java代码转换成Scala代码执行&a…

Apache中如何配置 ws 接口

Apache中如何配置 wss 接口 在Apache中配置WebSockets的支持&#xff0c;你需要使用mod_proxy_wstunnel模块&#xff0c;该模块是Apache的一个代理模块&#xff0c;它允许你代理WebSocket请求。 以下是配置步骤的简要说明和示例&#xff1a; 确保你的Apache服务器安装了mod_…

由于找不到msvcr80.dll,无法继续执行代码的解决方法

在日常使用电脑进行工作或娱乐时&#xff0c;您可能会遇到一个令人困惑的情况&#xff1a;屏幕上突然弹出一个错误提示&#xff0c;明确指出“msvcr80.dll文件丢失”&#xff0c;这个错误通常会导致某些应用程序无法正常运行。那么&#xff0c;当我们遇到这个问题时&#xff0c…

【Go 语言入门专栏】Go 语言的起源与发展

前言 Go 语言是当下最为流行的编程语言之一&#xff0c;大约在 2020、2021 年左右开始于国内盛行&#xff0c;许多大厂很早就将部分 Java 项目迁移到了 Go&#xff0c;足可看出其在性能方面的优越性。 相信各位都知道&#xff0c;在爬虫业务中&#xff0c;并发是一个关键的需…

Amazon云计算AWS之[4]非关系型数据库服务SimpleDB和DynamoDB

文章目录 简介非关系型VS关系数据库SimpleDB域条目属性值SimpleDB的使用 DynamoDBSimpleDB VS DynamoDB 简介 非关系型数据库服务主要用于存储结构化的数据&#xff0c;并为这些数据提供查找、删除等基本的数据库功能。AWS中提供的非关系型数据库主要包括SimpleDB和DynamoDB …

MySQL基础学习(待整理)

MySQL 简介 学习路径 MySQL 安装 卸载预安装的mariadb rpm -qa | grep mariadb rpm -e --nodeps mariadb-libs安装网络工具 yum -y install net-tools yum -y install libaio下载rpm-bundle.tar安装包&#xff0c;并解压&#xff0c;使用rpm进行安装 rpm -ivh \ mysql-communi…

嵌入式全栈开发学习笔记---vim编辑器

目录 进入vim编辑器 Shiftzz退出编辑器 vim的三种模式 命令模式 插入模式 yy复制一行 p粘贴一行 nyy复制多行 dd剪切一行 ndd剪切多行 x删除一个字符 nx删除多个字符 u撤销上一次操作 shiftg从第一行如果快速调到最后一行 gg回到第一行 pagedown & pageup往…

arp欺骗详解

目录 arp攻击原理 arp协议简介 arp攻击原理 arp实验 实验环境 实验步骤 1、使用ipconfig命令查看靶机&#xff08;window10&#xff09;的IP地址为下一步攻击做好准备&#xff0c;这一步是模拟你获取对方IP的过程 2、使用ifconfig查询查看攻击者&#xff08;kali&#x…