详解Tomcat中Servlet的加载过程(如何找到对应URL的Servlet处理程序)

1,722 阅读5分钟

首先要搞明白Web容器的架构:

(从容器的作用角度出发,从而解释为什么是这样的架构) 一个Web容器:

  1. 首先需要的是允许多种通信方式的请求,如HTTP,AJP(之后会讲AJP),所以我们需要一个Connector去负责处理多种协议,然后得到其中的信息并产生Request和Response对象存储这些信息; 举个栗子,如:http://localhost:8080/SecondServlet/HelloController,我们来看server.xml中是如何定义处理这个URL的Connector的,显然第一个是用于处理8080端口上的HTTP请求的Connector;
    <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
               redirectPort="8443" />
    <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
  1. 然后是对Connector处理得到的请求信息进行相应的处理程序的处理,所以我们要定位到相应的处理程序!如一个请求的URL是:http://localhost:8080/SecondServlet/HelloController localhost是主机名,SecondServlet是Web应用的名字,HelloController是Wrapper(对Servlet进行了一层封装),其在IDE里布局如下图:

而一个服务器上可能有多个主机(虚拟主机),一个主机有可能有多个应用,一个应用有可能有多个Wrapper; 虚拟主机是什么意思呢?《图解HTTP》中有张图:

即上图的三个域名经过DNS服务器查询返回的是同一个IP地址。 为了解决服务器存在三个“一对多”的现象,Tomcat架构据此进行了分层:

上图中Engine是对应的一个IP地址,Host是这个IP上的虚拟主机,Context是对应虚拟主机上的Web应用,Wrapper是这个Web应用里的Servlet处理程序,而这四个组件都被抽象成容器便于统一管理;
Connector和Engine被Service管理,一个Service对应多个Connector和一个Engine,因为Connector处理的多个协议得到的信息最后可能是通过同一个处理程序处理的;
那Tomcat是如何进行请求的向下传递的呢?答案是使用责任链模式(这个模式在HeadFirst设计模式中被放在了不受欢迎的一列,haha),这里就不展开讲了,这个模式挺简单的~

然后就是Tomcat是如何实现上面的这种流程的呢?(这个只能苦逼的看源码,对我来说还贼难,不过看完之后挺爽的)

  1. 首先我们来看在Tomcat中对上面四种容器的继承结构类图:

2. 然后来看处理的流程:首先请求是进入StandardEngine的,然后调用StandardEngineValve的invoke方法用于确认该请求对应的是哪一个虚拟主机:

    public final void invoke(Request request, Response response) throws IOException, ServletException {
        Host host = request.getHost();//从请求对象中得到Host的名字
        if (host != null) {
            if (request.isAsyncSupported()) {//这里是看该请求是否支持异步
                request.setAsyncSupported(host.getPipeline().isAsyncSupported());
            }

            host.getPipeline().getFirst().invoke(request, response);//然后调用相应Host的第一个invoke方法
        }
    }

此时请求已经转发到host中,具体代码如下:

public final void invoke(Request request, Response response) throws IOException, ServletException {
        Context context = request.getContext();
       //省略了非核心逻辑的校验代码
            try {
                    if (!response.isErrorReportRequired())
                    {
                    <!--此处调用了请求所对应的Context的invoke方法-->
                        context.getPipeline().getFirst().invoke(request, response);
                    }
                } 
            catch (Throwable var10) {
                    }
          }

此时请求已经转发到context中,此时只要对应到相应的Wrapper(Servlet)即可:

wrapper.getPipeline().getFirst().invoke(request, response);

最后来看如何获得我们所需要的真正的处理对象实例!

public final void invoke(Request request, Response response) throws IOException, ServletException {
    unavailable = false;
    Servlet servlet = null;
    StandardWrapper wrapper = (StandardWrapper)this.getContainer();
    if (!context.getState().isAvailable()) {
            response.sendError(503, sm.getString("standardContext.isUnavailable"));
            unavailable = true;//HTTP相应码为503表示当前Web应用正忙
        }
}
<!--如果当前Web应用空闲,则可以通过allocate()方法来获取Servlet对象-->
    if (!unavailable) {
            servlet = wrapper.allocate();
        }

下面这段代码很精髓,所以没办法只提取出关键性的逻辑: (this指代的是StandardWrapper)

    protected final AtomicInteger countAllocated = new AtomicInteger(0);
    protected volatile Servlet instance = null;//用volatile保证多线程环境下的可见性
    public Servlet allocate() throws ServletException {
        if (this.unloading) {
            throw new ServletException(sm.getString("standardWrapper.unloading", new Object[]{this.getName()}));//如果standWrapper已经被卸载,则抛出相应异常
        } else {
            boolean newInstance = false;
            Stack var2;
            //下面有点类似单例模式的双重校验实现方式,来保证多线程下只实例化一个Servlet对象
            //singleThreadModel表示是单线程模式,默认为false,即默认为多线程模式,该模式下只能实例化一个相同的Servlet对象,该变量在当前servlet实例实现了singleThreadModel时变为true;
            if (!this.singleThreadModel) {
                if (this.instance == null || !this.instanceInitialized) {
                    synchronized(this) {
                        if (this.instance == null) {
                            try {
                                if (this.log.isDebugEnabled()) {
                                    this.log.debug("Allocating non-STM instance");
                                }
                                //loadServlet用来加载相应的Servlet,下面有讲
                                this.instance = this.loadServlet();
                                newInstance = true;//改变这个变量说明此时已经有实例了
                                if (!this.singleThreadModel) {
                                //这里又加了一个判断条件,是用于防止刚被加载的实例又被其他线程卸载    this.countAllocated.incrementAndGet();
                                }
                            } 
                        }
                        //如果还没有被初始化,则进行初始化,此处注意没有初始化与没有被加载时不同的,因为没有初始化说明已经有实例了
                        if (!this.instanceInitialized) {
                            this.initServlet(this.instance);
                        }
                    }
                }
                //返回已经初始化完成的Servlet实例
                    return this.instance;
                }
        }
    }

下面是loadServletd的代码分析:(该方法也在StandardWrapper类中,一个StandWrapper类中只有一个Servlet)

//用于存储Wrapper对象中包装的Servlet名字
protected String servletClass = null;
public synchronized Servlet loadServlet() throws ServletException {
        if (!this.singleThreadModel && this.instance != null) {
            return this.instance;//在多线程模式下,如果实例已经存在了,则直接返回实例
        } else {
            Servlet servlet;
            try {
                InstanceManager instanceManager = ((StandardContext)this.getParent()).getInstanceManager();

                try {
                 //instanceManager中有该Warpper对应的Context的ClassLoader对象,然后根据Servletd的名字进行加载实例
                    servlet = (Servlet)instanceManager.newInstance(this.servletClass);
                } 
                this.initServlet(servlet);
            } 
            return servlet;
        }
    }

为什么loadServlet()方法中有initServlet(),而allocate()方法中既调用了loadServlet(),又再一次调用了initServlet()呢?

不行了,今天只能分析到此,脑子已经理不过来了,不过具体的流程已经很明朗了,总结一下:

首先Engine从Connector中获取到Request对象和Response对象,然后从Engine转发到对应的Host,然后转发到对应的Context,然后对应的Wrpper负责加载对应的Servlet,其中又涉及到很多多线程下的安全问题,如何保证只加载一个Servlet实例并进行初始化。

参考链接: www.jianshu.com/p/1a2d2f868…