带着如下问题学习本文内容:
1、以Tomcat为例,说说Servlet容器是如何工作的;
2、一个Web工程是如何在Servlet容器中启动的;
3、Servlet容器是如何解析你的web.xml中定义的Servlet;
4、用户的请求是如何被分配给指定的Servlet的;
5、Servlet容器是如何管理Servlet生命周期的;
6、Servlet的API类层次结构,以及如何分析Servlet中的一些难点问题;
1 Servlet容器
Servlet容器作为一个独立发展的标准化产品,其存在的意义是为了适应工厂化生产,通过标准化接口来相互协作,实现Servlet和Servlet容器之间的解耦。
tomcat容器模型如下图所示:
由上图可知,Tomcat的容器分为4个等级,真正管理Servlet容器的是Context容器,一个Context对应一个Web工程。
1.1 Servlet容器的启动过程
当添加一个应用时将会创建一个StandardContext容器,并且给这个容器设置一些必要的参数,如:url和path分别代表这个应用在tomcat的访问路径和这个应用的实际物理路径,另外一个非常重要的配置是ContextConfig,这个类将会负责整个web应用配置的解析工作,最后将这个context容器加到父容器Host中。
tomcat主要类的启动时序图如下所示:
由上图可知:tomcat的通过start()方法启动tomcat,而tomcat的启动逻辑是基于观察者模式设计的,所有的容器都会继承lifecycle接口,由他管理容器的整个生命周期,所有容器的修改和状态的改变都会由他去通知已经注册的观察者。
当context容器初始化状态为init时,添加在context容器的listener将会被触发,而ContextConfig继承了LifecycleListener接口,因此它也就被加入到StandardContext容器中,负责整个web应用的配置文件解析工作。
ContextConfig的init方法主要完成以下工作:
- 创建用于解析xml配置文件的contextDigester对象
- 读取默认context.xml配置文件,解析
- 读取默认host配置文件,解析
- 读取默认context配置文件,解析
- 设置context的docBase
ContextConfig的init方法完成后,context容器会执行startInternal方法,主要完成以下任务:
1、创建读取资源文件的对象
2、创建ClassLoader对象
3、设置应用的工作目录
4、启动相关的辅助类如:logger realm resources等
5、修改启动状态,通知感兴趣的观察者(web应用的配置)
6、子容器的初始化
7、获取ServletContext并设置必要的参数
8、初始化"load on startup"的servlet
1.2 Web应用的初始化工作
Web应用的初始化工作是在ContextConfig的configureStart方法中实现的,主要是为了解析web.xml文件。
在web.xml文件中的各个配置项将会被解析成相应的属性,保存在WebXml对象中。
接下来会将WebXml对象中的属性设置到Context容器中,这里包括创建Servlet对象、filter、listener等。
提问:为什么要将servlet包装成StandardWrapper?
因为StandardWrapper是tomcat容器中的一部分,具有容器的特征,而servlet是一个独立的web开发标准,不应该强耦合到tomcat中。
除了将servlet包装成StandardWrapper并作为子容器添加到context中,其他的所有web.xml属性都被解析到context中,所以说context容器才是真正运行servlet的容器,一个web应用对应一个context容器,容器的配置属性由应用的web.xml指定。
2 创建Servlet实例
2.1 创建Servlet对象
如果Servlet的load-on-startup配置项大于0,那么在Context容器启动时就会被实例化。
创建Servlet实例的方法是从Wrapper.loadServlet开始的,loadServlet方法要完成的就是获取servletClass,然后把它交给InstanceManager去创建一个基于servletClass.class的对象,如果这个Servlet配置了jsp-file,那么这个servletClass就是在conf/web.xml中定义的org.apache.jasper.servlet.JspServlet了。
创建Servlet对象的相关类结构图如下所示:
2.2 初始化Servlet
初始化Servlet在StandardWrapper的initServlet方法中,通过调用Servlet的init方法,同时把包装了StandardWrapper对象的StandardWrapperFacade作为ServletConfig传给Servlet。
private synchronized void initServlet(Servlet servlet) throws ServletException {
if (!this.instanceInitialized || this.singleThreadModel) {
try {
if (Globals.IS_SECURITY_ENABLED) {
boolean success = false;
try {
Object[] args = new Object[]{this.facade};
SecurityUtil.doAsPrivilege("init", servlet, classType, args);
success = true;
} finally {
if (!success) {
SecurityUtil.remove(servlet);
}
}
} else {
servlet.init(this.facade);
}
this.instanceInitialized = true;
} catch (UnavailableException var10) {
this.unavailable(var10);
throw var10;
}
//省略catch
}
}
3 Servlet体系结构
Servlet顶层类关联图如下:
与Servlet主动关联的是ServletConfig、ServletRequest和ServletResponse。这三个类都是通过容器传递给Servlet的,其中ServletConfig在Servlet初始化时就传递给Servlet了,ServletConfig中的方法主要是为了获取这个Servlet的一些配置属性,而这些属性可能在Servlet运行时被用到;
ServletContext可以认为是一个全局的存储信息的空间,所有用户共享一个ServletContext,tomcat启动就存在,tomcat关闭才释放;
ServletRequest和ServletResponse是实现数据交互的具体对象,可以理解为运输工具用来传递交互结果;
ServletConfig和ServletContext在Tomcat容器中的类关系图:
可以看出,StandardWrapper和StandardWrapperFacade都实现了ServletConfig接口,而StandardWrapperFacade是StandardWrapper门面类,所以传给Servlet的是StandardWrapperFacade对象,这个类能够保证从StandardWrapper中拿到ServletConfig所规定的数据,而又不把ServletConfig不关心的数据暴露给Servlet。
同样,ServletContext和ServletConfig也有类似的结构,在Servlet中能拿到的ServletContext的实际对象也是ApplicationContextFacade对象,ApplicationContextFacade同样保证ServletContext只能从容器中拿到它该拿的数据,这是典型的门面设计模式。
我们在创建自己的Servlet类时通常使用的是HttpServletRequest和HttpServletResponse,它们继承了ServletRequest和ServletResponse。从Context容器中传过来的ServletRequest和ServletResponse可以直接被转化为HttpServletRequest和HttpServletResponse。
通过分析Tomcat的Request和Response类结构图可以得知:Tomcat一接收到请求首先会创建org.apache.coyote.Request和org.apache.coyote.Response,用来将请求快速分配给后续线程去处理,然后用户线程去处理这个请求时又创建org.apache.catalina.connector.Request和org.apache.catalina.connector.Response对象,直到传给Servlet,但是实际上传给Servlet的是Request和Response的门面类RequestFacade和ResponseFacade,所以我个人认为这也就解释了为何Context容器传过来的ServletRequest、ServletResponse可以被转化为HttpServletRequest和HttpServletResponse了。
4 Servlet如何工作
当用户从浏览器向服务器发起一个请求通常会包含以下信息:http://hostname:port/contextpath/servletpath,其中:hostname和port用来与服务器建立TCP连接,后面的URL用来选择在服务器中哪个子容器来服务用户的请求。
Tomcat是如何实现根据URL来到达正确的Servlet容器中的呢?
在Tomcat7中通过一个专门的映射类来完成,这个映射类就是org.apache.tomcat.util.http.mapper,这个类保存了Tomcat的Container容器中的所有子容器的信息,org.apache.catalina.contnector.Reqeuest类在进入Container容器之前,Mapper将会根据这次请求的hostname和contextpath将host和context容器设置到Request的mappingData属性中,所以当Request进入Container容器之前,对于它要访问哪个子容器就已经确定了。
之所以Mapper中会有容器的完整关系,还是多亏了MapperListener类,其核心方法如下:
public void startInternal() throws LifecycleException {
this.setState(LifecycleState.STARTING);
Engine engine = this.service.getContainer();
if (engine != null) {
this.findDefaultHost();
this.addListeners(engine);
Container[] conHosts = engine.findChildren();
Container[] arr$ = conHosts;
int len$ = conHosts.length;
for(int i$ = 0; i$ < len$; ++i$) {
Container conHost = arr$[i$];
Host host = (Host)conHost;
if (!LifecycleState.NEW.equals(host.getState())) {
this.registerHost(host);
}
}
}
}
这段代码的作用就是将MapperListener类作为一个监听者加到整个Container容器的每个子容器中,这样只要任何一个容器发生变化,MapperListener都将会被通知到,在for循环中就是将host及下面的子容器注册到mapper中。
请求到达最终的Wrapper容器后,接下来就是要执行Fiter链以及通知你在web.xml中定义的listener,然后执行Servlet的service方法。
当servlet从容器中移除时,也就意味着servlet的生命周期结束了,这次servlet的destroy方法将被调用。
5 Servlet中的Listener
Listener是基于观察者模式设计的,能够方便地从另一个纵向维度控制程序和数据。
详见:https://yq.aliyun.com/articles/636239?type=2
6 Filter如何工作
Fiter简介
Fiter虽然不是一个Servlet,不能产生response,但是它能够在一个request到达servlet之前预处理request,也可以在response离开servlet时处理response;
主要用途是:比如过滤一些敏感内容、编码过滤、URL权限控制等等。
工作原理
生命周期
Filter接口中有三个重要的方法:
init()方法:初始化参数,在创建Filter时自动调用。当我们需要设置初始化参数的时候,可以写到该方法中。
doFilter()方法:拦截到要执行的请求时,doFilter就会执行。这里面写我们对请求和响应的预处理。
destroy()方法:在销毁Filter时自动调用,当web容器调用这个方法之后,容器还会再调用一次doFilter方法。
Filter的创建和销毁由web服务器来控制。
- 服务器启动的时候,web服务器创建Filter的实例对象,并调用其init方法,完成对象的初始化功能,filter对象只会创建一次,init方法也只会执行一次;
- 拦截到请求时,执行doFilter方法,可以多次执行;
- 服务器关闭时,web服务器销毁Filter的实例对象。
Filter类的核心还是FilterChain对象,这个对象保存了到最终Servlet对象等待所有Filter对象,这些对象都保存再ApplicationFilterChain对象的filters数组中,再FilterChain链上每执行一个Filter对象,数组的当前计数都会加1,直到计数等于数组的长度。当FilterChain上所有的Filter对象执行完成后,就会执行最终的servlet,所以在ApplicationFilterChain对象中会持有Servlet对象的引用。
Servlet中的url-pattern
在web.xml中和中都有配置项,其作用是匹配一次请求是否会执行这个Servlet或者Filter。
Filter的url-pattern匹配是在创建ApplicationFilterChain对象时进行的,它会把所有定义的Filter的url-pattern与当前的URL匹配,如果匹配成功就将这个Filter保存到ApplicationFilterChain的filters数组中,然后在FilterChain中依次调用。