learn-tech/专栏/深入拆解TomcatJetty/26Context容器(下):Tomcat如何实现Servlet规范?.md
2024-10-16 09:22:22 +08:00

12 KiB
Raw Permalink Blame History

                        因收到Google相关通知网站将会择期关闭。相关通知内容
                        
                        
                        26 Context容器Tomcat如何实现Servlet规范
                        我们知道Servlet容器最重要的任务就是创建Servlet的实例并且调用Servlet在前面两期我谈到了Tomcat如何定义自己的类加载器来加载Servlet但加载Servlet的类不等于创建Servlet的实例类加载只是第一步类加载好了才能创建类的实例也就是说Tomcat先加载Servlet的类然后在Java堆上创建了一个Servlet实例。

一个Web应用里往往有多个Servlet而在Tomcat中一个Web应用对应一个Context容器也就是说一个Context容器需要管理多个Servlet实例。但Context容器并不直接持有Servlet实例而是通过子容器Wrapper来管理Servlet你可以把Wrapper容器看作是Servlet的包装。

那为什么需要Wrapper呢Context容器直接维护一个Servlet数组不就行了吗这是因为Servlet不仅仅是一个类实例它还有相关的配置信息比如它的URL映射、它的初始化参数因此设计出了一个包装器把Servlet本身和它相关的数据包起来没错这就是面向对象的思想。

那管理好Servlet就完事大吉了吗别忘了Servlet还有两个兄弟Listener和Filter它们也是Servlet规范中的重要成员因此Tomcat也需要创建它们的实例也需要在合适的时机去调用它们的方法。

说了那么多下面我们就来聊一聊Tomcat是如何做到上面这些事的。

Servlet管理

前面提到Tomcat是用Wrapper容器来管理Servlet的那Wrapper容器具体长什么样子呢我们先来看看它里面有哪些关键的成员变量

protected volatile Servlet instance = null;

毫无悬念它拥有一个Servlet实例并且Wrapper通过loadServlet方法来实例化Servlet。为了方便你阅读我简化了代码

public synchronized Servlet loadServlet() throws ServletException { Servlet servlet;

//1. 创建一个Servlet实例
servlet = (Servlet) instanceManager.newInstance(servletClass);    

//2.调用了Servlet的init方法这是Servlet规范要求的
initServlet(servlet);

return servlet;

}

其实loadServlet主要做了两件事创建Servlet的实例并且调用Servlet的init方法因为这是Servlet规范要求的。

那接下来的问题是什么时候会调到这个loadServlet方法呢为了加快系统的启动速度我们往往会采取资源延迟加载的策略Tomcat也不例外默认情况下Tomcat在启动时不会加载你的Servlet除非你把Servlet的loadOnStartup参数设置为true。

这里还需要你注意的是虽然Tomcat在启动时不会创建Servlet实例但是会创建Wrapper容器就好比尽管枪里面还没有子弹先把枪造出来。那子弹什么时候造呢是真正需要开枪的时候也就是说有请求来访问某个Servlet时这个Servlet的实例才会被创建。

那Servlet是被谁调用的呢我们回忆一下专栏前面提到过Tomcat的Pipeline-Valve机制每个容器组件都有自己的Pipeline每个Pipeline中有一个Valve链并且每个容器组件有一个BasicValve基础阀。Wrapper作为一个容器组件它也有自己的Pipeline和BasicValveWrapper的BasicValve叫StandardWrapperValve。

你可以想到当请求到来时Context容器的BasicValve会调用Wrapper容器中Pipeline中的第一个Valve然后会调用到StandardWrapperValve。我们先来看看它的invoke方法是如何实现的同样为了方便你阅读我简化了代码

public final void invoke(Request request, Response response) {

//1.实例化Servlet
servlet = wrapper.allocate();

//2.给当前请求创建一个Filter链
ApplicationFilterChain filterChain =
    ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);

//3. 调用这个Filter链Filter链中的最后一个Filter会调用Servlet filterChain.doFilter(request.getRequest(), response.getResponse());

}

StandardWrapperValve的invoke方法比较复杂去掉其他异常处理的一些细节本质上就是三步

第一步创建Servlet实例 第二步给当前请求创建一个Filter链 第三步调用这个Filter链。

你可能会问为什么需要给每个请求创建一个Filter链这是因为每个请求的请求路径都不一样而Filter都有相应的路径映射因此不是所有的Filter都需要来处理当前的请求我们需要根据请求的路径来选择特定的一些Filter来处理。

第二个问题是为什么没有看到调到Servlet的service方法这是因为Filter链的doFilter方法会负责调用Servlet具体来说就是Filter链中的最后一个Filter会负责调用Servlet。

接下来我们来看Filter的实现原理。

Filter管理

我们知道跟Servlet一样Filter也可以在web.xml文件里进行配置不同的是Filter的作用域是整个Web应用因此Filter的实例是在Context容器中进行管理的Context容器用Map集合来保存Filter。

private Map<String, FilterDef> filterDefs = new HashMap<>();

那上面提到的Filter链又是什么呢Filter链的存活期很短它是跟每个请求对应的。一个新的请求来了就动态创建一个Filter链请求处理完了Filter链也就被回收了。理解它的原理也非常关键我们还是来看看源码

public final class ApplicationFilterChain implements FilterChain {

//Filter链中有Filter数组这个好理解 private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];

//Filter链中的当前的调用位置 private int pos = 0;

//总共有多少了Filter private int n = 0;

//每个Filter链对应一个Servlet也就是它要调用的Servlet private Servlet servlet = null;

public void doFilter(ServletRequest req, ServletResponse res) { internalDoFilter(request,response); }

private void internalDoFilter(ServletRequest req, ServletResponse res){

// 每个Filter链在内部维护了一个Filter数组
if (pos < n) {
    ApplicationFilterConfig filterConfig = filters[pos++];
    Filter filter = filterConfig.getFilter();

    filter.doFilter(request, response, this);
    return;
}

servlet.service(request, response);

}

从ApplicationFilterChain的源码我们可以看到几个关键信息

Filter链中除了有Filter对象的数组还有一个整数变量pos这个变量用来记录当前被调用的Filter在数组中的位置。 Filter链中有个Servlet实例这个好理解因为上面提到了每个Filter链最后都会调到一个Servlet。 Filter链本身也实现了doFilter方法直接调用了一个内部方法internalDoFilter。 internalDoFilter方法的实现比较有意思它做了一个判断如果当前Filter的位置小于Filter数组的长度也就是说Filter还没调完就从Filter数组拿下一个Filter调用它的doFilter方法。否则意味着所有Filter都调到了就调用Servlet的service方法。

但问题是方法体里没看到循环谁在不停地调用Filter链的doFilter方法呢Filter是怎么依次调到的呢

答案是Filter本身的doFilter方法会调用Filter链的doFilter方法我们还是来看看代码就明白了

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain){

      ...
      
      //调用Filter的方法
      chain.doFilter(request, response);
  
  }

注意Filter的doFilter方法有个关键参数FilterChain就是Filter链。并且每个Filter在实现doFilter时必须要调用Filter链的doFilter方法而Filter链中保存当前Filter的位置会调用下一个Filter的doFilter方法这样链式调用就完成了。

Filter链跟Tomcat的Pipeline-Valve本质都是责任链模式但是在具体实现上稍有不同你可以细细体会一下。

Listener管理

我们接着聊Servlet规范里Listener。跟Filter一样Listener也是一种扩展机制你可以监听容器内部发生的事件主要有两类事件

第一类是生命状态的变化比如Context容器启动和停止、Session的创建和销毁。 第二类是属性的变化比如Context容器某个属性值变了、Session的某个属性值变了以及新的请求来了等。

我们可以在web.xml配置或者通过注解的方式来添加监听器在监听器里实现我们的业务逻辑。对于Tomcat来说它需要读取配置文件拿到监听器类的名字实例化这些类并且在合适的时机调用这些监听器的方法。

Tomcat是通过Context容器来管理这些监听器的。Context容器将两类事件分开来管理分别用不同的集合来存放不同类型事件的监听器

//监听属性值变化的监听器 private List