学习参考资料
(1)Servet 3.1 final 规范;
(2)《Java Web高级编程》;
(3)《深入分析Java Web技术内幕》(第2版)

心得:虽然现在是实际工作中很少直接使用Servlet,但了解Servlet规范中对不同组件(Servlet,Filter,Listener等等)以及Servlet容器的实现对于基于Servlet的Java EE应用的理解也是大有益处的。因此基于上面3个资料的学习所得以及我自己阅读Tomcat 8相关部分源码的一些收获在这里总结记录一下。

1. Servlet容器

Servlet 容器是 web server 或 application server 的一部分,提供基于请求/响应发送模型的网络服务,解码基于 MIME 的请求,并且格式化基于 MIME 的响应。Servlet 容器也包含了管理 Servlet 生命周期。

2. Servlet

Servlet 是基于 Java 技术的 web 组件,被容器所托管的,用于生成动态内容。像其他基于 Java 的组件技术一样,Servlet 也是基于平台无关的 Java 类格式,被编译为平台无关的字节码,可以被基于 Java 技术的 web server 动态加载并运行。

2.1 Servlet的数量

Servlet默认是线程不安全的,一个容器中只有每个servlet一个实例,但是如果实现了SingleThreadModule接口,容器将实现多个servlet实例
SingleThreadModule也不能保证线程安全,它只能保证任意两个线程不会使用同一个Servlet实例(可能由一个对象池来维护),servlet2.4已经将这个接口已经标注为已过时了;

我查看了Tomcat 8.0中StandardWrapper源码,这个类负责Servlet的创建,其中SingleThreadModule模式下创建的实例数不能超过20个,也就是同时只能支持20个线程访问这个Serlvet,因此,这种对象池的设计会进一步限制并发能力和可伸缩性。

2.2 servlet的生命周期

加载和实例化:servlet容器负责加载和实例化Servlet,在容器启动时根据设置决定是在启动时初始化(loadOnStartup大于等于0在容器启动时进行初始化,值越小优先级越高),还是延迟初始化致第一次请求前;

初始化:

init(),执行一些一次性的动作,可以通过ServletConfig配置对象,获取初始化参数,访问ServletContext上下文环境;
初始化时可能发生错误,UnavailableException和ServletException,那么servlet不应放置活动服务中,未成功初始化,destroy方法也应被调用

请求处理:

servlet容器封装Request和Response对象传给对应的servlet的service方法,对于HttpServlet,就是HttpServletRequest和HttpServletResponse;
HttpServlet中使用模板方法模式service方法根据HTTP请求方法进一步分派到doGet,doPost等不同的方法来进行处理;

对于HTTP请求的处理,只有重写了支持HTTP方法的对应HTTP servlet方法(doGet),才可以支持,否则放回405(Method Not Allowed)。

线程不安全
servlet中默认线程不安全,单例多线程,因此对于共享的数据(静态变量,堆中的对象实例等)自己维护进行同步控制,不要在service方法或doGet等由service分派出去的方法,直接使用synchronized方法,很显然要根据业务控制同步控制块的大小进行细粒度的控制,将不影响线程安全的耗时操作移出同步控制块;

异常
请求处理时同样可能抛出异常,UnavailableException和ServletException;
UnavailableException表示不可用,永久不可用状态返回404;暂时不可用返回503(服务不可用),标注Retry-After头;

异步处理:

Servlet中等待是一个低效的操作,因为这是阻塞操作。
异步处理请求能力,使线程可以返回到容器,从而执行更多的任务。当开始异步处理请求时,另一个线程或回调可以:(1)产生响应;或者,(2)请求分派;或者,(3)调用完成;

关键方法:

启用:让servlet支持异步支持:asyncSupported=true;

启动:AsyncContextasyncContext=req.startAsyncContext();或startAsyncContext(req,resp);

完成:asyncContext.complete();必须在startAsync调用之后,分派进行之前调用;同一个AsyncContext不能同时调用dispatch和complete

分派:asyncContext.dispatch();dispatch(Stringpath);dispatch(ServletContextcontext,Stringpath);
不能在complete之后调用;
从一个同步servlet分派到异步servlet是非法的;

超时:asyncContext.setTimeout(millis);
超时之后,将不能通过asyncContext进行操作,但是可以执行其他耗时操作;
在异步周期开始后,容器启动的分派已经返回后,调用该方法抛出IllegalStateException;如果设置成0或小于0就表示notimeout;
超时表示HTTP连接已经结束,HTTP已经关闭,请求已经结束了。

启动新线程
通过AsyncCOntext.start(Runnable)方法,向线程池提交一个任务,其中可以使用AsyncContext(未超时前);

事件监听:addListener(newAsyncListener{…});
onComplete:完成时回调,如果进行了分派,onComplete方法将延迟到分派返回容器后进行调用;
onError:可以通过AsyncEvent.getThrowable获取异常;
onTimeout:超时进行回调;
onStartAsync:在该AsyncContext中启动一个新的异步周期(调用startAsyncContext)时,进行回调;

超时和异常处理,步骤:
(1)调用所有注册的AsyncListener实例的onTimeout/onError;
(2)如果没有任何AsyncListener调用AsyncContext.complete()或AsyncContext.dispatch(),执行一个状态码为HttpServletResponse
.SC_INTERNAL_SERVER_ERROR出错分派;
(3)如果没有找到错误页面或者错误页面没有调用AsyncContext.complete()/dispatch(),容器要调用complete方法;

终止:

servlet容器确定从服务中移除servlet时,可以通过调用destroy()方法将释放servlet占用的任何资源和保存的持久化状态等。调用destroy方法之前必须保证当前所有正在执行service方法的线程执行完成或者超时;
之后servlet实例可以被垃圾回收,当然什么时候回收并不确定,因此destroy方法是是否必要的。

2.3 Servlet(Filter)中的url-pattern

Serlvet和Filter有三种不同的匹配规则:
(1)精确匹配:/foo;
(2)路径匹配:/foo/*;
(3)后缀匹配:*.html;
Serlvet的匹配顺序是:
首先进行精确匹配;如果不存在精确匹配的进行路径匹配;最后根据后缀进行匹配;一次请求只会匹配一个Servlet;(Filter是只要匹配成功就添加到FilterChain)

PS:其他写法(/foo/,/*.html,*/foo)都不对;“/foo*”不能匹配/foo,/foox;

3. Request

3.1 HTTP协议参数

通过HttpServletRequest对象获取Http参数:
getParameter,getParameterNames,getParameterValues,getParameterMap;

这些方法从getRequestURI方法或getPathInfo方法返回的字符串值中解析,如果是POST方法,也是在第一次调用getParameter方法时候进行解码获取到参数集合当中,因此要在调用这些方法之前设置编解码方式,否则可能导致乱码;

POST表单数据也会被汇总到请求参数集合中,但要满足:
(1)Content-Type必须是application/x-www-form-urlencoded;
(2)进行getParameter调用;
如果不满足获取POST参数的条件,servlet可以通过request对象的输入流得到POST数据;相反如果满足条件,输入流中也不再可以读取POST数据(因为已经读取过了);

3.2 文件上传

数据以multipart/form-data格式发送,servlet支持文件上传;
通过 HttpServletRequest的:
public Collection<Part> getParts()
public Part getPart(String name)

每个Part类代表从multipart/form-data格式的POST请求中接受的一个部分或表单项,每个Part可以通过Part.getInputStream方法访问头部,内容类型和内容;

对于表单数据的Content-Disposition,即使没有文件名,也可使用part的名称通过HttpServletRequest的getParameter和getParameterValues得到part的字符串值;

3.3 属性:

属性的作用域与请求相关;
getAttribute/getAttributeNames/setAttribute;

3.4 请求路径元素

对于这样的请求各个部分是怎样的:http://localhost:8080/example/servlets/servlet/空幻?author=空幻#success

Context Path:ServletContext关联路径,getContextPath,“/example”;
Servlet Path:getServletPath,“/servlets/servlet”,请求“/*”与“”模式匹配对应的servlet path是空字符串;
PathInfo:请求路径一部分,不属于Content Path或Servlet Path,“/空幻”,要么为null,要么为以“/”开头的字符串;
Request URI:getRequestURI,等于contetPath + servletPath + pathInfo;
QueryString:getQueryString,“author=空幻”;
Request URLhttp://localhost:8080/example/servlets/servlet/空幻;

路径转换方法

ServletContext.getRealPath
HttpServletRequest.getPathTranslated
比如:
(1)“http://localhost:8080/s/request/pathinfo”, 在我的机器上`getPathTranslated()返回“/home/yjh/wks/workspace/ServletTest/target/servletTest/pathinfo”;
其中”/request”是serlvet path,“servletTest”是项目根目录名;这两个方法都是基于项目根目录返回的;

3.5 Servlet 3.1新特性,非阻塞I/O

非阻塞I/O只能用在Serlvet和Filter的异步请求处理升级处理中; 否则设置时抛出IllegalStateException;

Request——ServletInputStream——ReadListener;
Response——ServletOutputStream——WriterListener;

ReadListener:
(1)onDataAvailable:当可以从传入请求流中读取数据,onDataAvailable将被调用,和ServletInputStream.isReady相关;
(2)onAllDataRead:读取完成ServletRequest的所有数据时调用onAllDataRead方法,和ServletInputStream.isFinished()相关;
(3)onError(Throwable);

3.6 请求数据编码

getParameter等参数获取方法会将参数部分从流中读取出来,因此一定要在getParameter调用前设置编解码方式:

Request:
setCharacterEncoding()

Response:
setCharacterEncoding()
setHead()
setContentType()

下面在Response总结中会进一步说明编码和响应及其缓冲区之间的关系.

3.7 Request 对象的生命周期

每个Request对象在Servlet的service(这就包括JSP的表达式,脚本,声明),Filter的doFilter的作用域中有效;

启用了异步处理后,request对象将到AsyncContext的complete调用时;

4. ServletContext接口

每个基于Servlet的Web应用都有自己的ServletContext保存和维护自己的上下文信息,包括:初始化参数,Servlet,Filter,Listener配置,容器属性等等。

4.1 配置

主要有3种方式:
(1)Web.xml部署描述符;
(2)注解;
(3)通过ServletContextListener/ServletContainerInitializer使用Servlet/Filter的Registration配置;

4.2 上下文属性

容器也有自己的属性,这里提一下是因为这涉及到:
(1)EL表达式的隐式变量作用域:applicationScope包含所有绑定到ServletContext的特性;EL表达式中变量的作用域也是一层层查找的,最后一层查找范围就是ServletContext的特性;
(2)同样JSP中的隐式变量application也是ServletContext实例;

4.3 资源

获取Web应用下的资源:
getResourcegetResourceAsStream
传入path,必须要以“/”开头,相对与两个目录:上下文的根目录web应用的WEB-INF/lib中的JAR文件中的META-INF/resources目录。依次查找这两个地方;

这两个方法不能获取动态内容,比如jsp,获取的是jsp文件源码而不是处理后的响应;

4.4 临时工作目录

Servlet容器必须为每一个servlet上下文提供一个私有的临时目录,并将通过javax.servlet.context.tempdir上下文属性使其可用,该属性关联的是java.io.File。
这个目录也是Multipart处理中临时目录的默认目录,并且location如果是相对路径也是基于它的。

5. Response

Response的getWritergetOutputStream在同一次请求中不能同时被调用。调用了一个之后在调用另一个会抛出IllegalStateException;

5.1 缓冲

获取和设置缓冲区大小:getBufferSize和setBufferSize,不能在缓冲区写入内容之后设置缓冲区大小调用setBufferSize;

PS:tomcat 8中缓冲区大小为8192

是否提交到客户端:isCommitted;

刷新缓冲区:flushBuffer,也可以通过getWriter/getOutputStream调用输出流的flush;

重置缓冲区:reset和resetBuffer,不能在响应提交后调用,否则抛出IllegalStateException,响应及关联的缓冲区不变;

PS:一般并不需要进行手动刷新缓冲区,service方法结束或请求处理完成后,容器会自动刷新缓冲区.但如果使用异步处理分派的话,Response的生命周期其实已经延伸到了开始异步的service方法之外了,这样如果你想要在service方法返回前提交响应则可以手动刷新缓冲区,否则只能等到异步完成/超时请求处理结束或者缓冲区满了才能提交到客户端了.

5.2 重定向和设置Error

sendRedirectsendError
这两方法有一些相似性:
(1)如果在调用前已有响应提交到客户端,调用它们将抛出IllegalStateException;
(2)如果没有响应提交,sendRedirect和sendError将重置缓冲区,舍弃原来缓冲区中的旧数据,Servlet中之后的输出也是无效的(将被忽略);

5.3 Response编码和国际化

同样需要在响应未提交或resp.getWriter()之前进行设置,否则将无效(面向字符的输出已经设置默认编码);

国际化配置
在部署描述符中配置,如果没有配置将使用容器依赖的mapping等配置:

    <locale-encoding-mapping-list>
        <locale-encoding-mapping>
            <locale>zh_CN</locale>
            <encoding>UTF8</encoding>
        </locale-encoding-mapping>
    </locale-encoding-mapping-list>

setLocale也可以设置编码,在setContentTypesetCharacterEncoding之前,调用setLocale设置编码,使用上面配置中的编码;但是这并不会设置HTTP响应头的content-type等头,因此浏览器/客户端将使用默认的解码方式来解码这可能导致乱码;

PS:setLocale将通过Content-Language响应头来传递;但是编码方式如果没有指定Content-Type,是不能通过HTTP header传递的;

因此,应该在getWriter方法被调用或响应被提交之前通过setContentTypesetCharacterEncodingaddHeader设置编码方式,否则将使用默认编码:ISO-8859-1;

setCharacterEncoding:这个方法可以覆盖setLocalesetContentType设置的编码方式,但不会设置Content-Type头;

setLocalesetCaracterEncodingsetContentType都可以设置编码方式,但是要通过setContentTypeaddHeader设置Content-Type响应头,并且它们都要在getWriter调用前或响应提交前设置;

5.4 结束响应对象

以下时间表明servlet满足了请求且响应对象即将关闭“
(1)servlet的service方法终止;
(2)响应的setContentLengthsetContentLong制定了大于零的内容量,且已经写入到响应;
(3)sendError方法或sendRedirect方法已调用;
(4)AsyncContext的compelete方法已调用;

setContentLengthsetContentLengthLong方法一般有Web容器在响应完成后负责调用,后者是Servlet3.1的新方法;

5.5 Response生命周期

和Request相似,在servlet的service方法和Filter的doFilter方法内有效,如果启动异步处理,直到complete方法被调用有效。

6. Filter

6.1 对Filter的理解

FilterServlet/其他Web资源(包括静态资源)组合起来使用,实现了一个职责链模式的请求处理调用栈,Servlet/Web资源是最后一个“入栈的节点”(当然Filter可以阻止请求到达Servlet/Web资源,)。Filter可以在servlet调用前和调用后进行一些额外的处理过程(比如,验证,日志,压缩等等)。

FilterChain.doFilter(req, resp)调用前后,正分别是调用栈“入栈”和“出栈”之时,做相应的处理。

每个Filter配置对应的每个JVM的容器仅实例化一个实例。

6.2 Filter的生命周期

(1)init()/init(FilterConfig filterConfig):和Servlet一样可能抛出UnavaliableException(暂时不可用/永久不可用);init()方法总是在应用程序启动时调用(ServletContextListener初始化之后,Servlet初始化之前);
(2)doFilter():服务中,处理传入请求和返回响应:可以进行检查请求头,修改请求头/数据,修改响应头等等;

Filter可以调用chain.doFilter()方法调用过滤器链中下一个实体;也可以不调用来阻止请求;

doFilter过程中也可能抛出UnavailableException,容器负责停止处理剩下的过滤器链,若不是永久不可用,可以选择稍后重试整个链。

(3)destroy():容器把服务中的Filter实例移除前,先调用它的destroy方法,进行释放资源等清理工作;

6.3 Filter的类型

Servlet容器中,存在多种分派方式,Servlet2.4之后,可以对不同的请求分派进行过滤:
(1)普通请求
(2)转发请求RequestDispatcher.forward()<jsp:forward>触发的请求,这种转发,本质上是服务器应用内部的方法调用;
(3)包含请求RequestDispatcher.include()<jsp:include>,注意这种是包含输出和<%@ inlcude %>静态导入的区别;
(4)错误资源请求:发生异常,请求错误页面;
(5)异步请求:如果要结合异步处理的Servlet使用,Filter同样也要开启支持异步处理。这里异步请求指的是有AsyncContext派发的请求,实现异步过滤器要注意可能被单个异步请求调用多次(潜在的多个不同线程);

在部署描述符中,通过<dispatcher>元素中可以选择Filter支持的请求类型。

6.4 Filter的配置

初始化参数的设置
同样Filter也可以通过编程注解XML三种方式配置;

到Serlvet 3.1,注解配置Filter不能保证设置顺序。

Filter的顺序
(1)基本顺序:
首先<url-pattern>匹配,按照Filter在部署描述符中出现的顺序匹配过滤器映射;
其次再按照<serlvet-name>出现的顺序匹配;
(2)编程设置Filter顺序:

    registration.addMappingForUrlPatterns(null, false, "/foo", "bar/*");

第二个参数表示是否在部署描述符之后的Filter之后

6.5 Servlet,Filter和UnavailableException

UnavailableException表示Servlet或Filter不可用,这种情况一般Servlet容器负责处理,重试或者返回响应;
UnavailableException的分为两种:
(1)永久不可用:比如servlet配置不正确或者Filter状态异常。
(2)暂时不可用:可能由于一些system-wide的问题导致请求无法处理,比如第三方服务不可用,内存和磁盘不足等等;可以在稍后重试;

7. Servlet及其容器的工作原理(Tomcat 8.0为例)

Tomcat分为4层结构:Container容器->Engine容器->Host容器->Servlet容器;

一个请求,根据它的URL,Tomcat将根据它的Host,Context一层层将其转发到合适的Servlet(对于很多MVC是映射到一个Servlet,在根据之后的pathinfo解析分派到对应的处理函数)。

一个Context对应一个Web工程:

    <Context path="/projectOne" docBase="/home/xxx/xxx" reloadable="true" />

7.1 Servlet容器的启动

这里的config也是通过Tomcat.addWebApp的重载版本中调用构造器创建的,传入该方法进行设置;contextPath和docBase分别对应Web应用的访问路径物理路径
(1)新增Web应用,设置访问路径,工作目录监听器,创建注入ContextConfig对象;

    public Context addWebapp(Host host, String contextPath, String docBase, ContextConfig config) {
        silence(host, contextPath);

        Context ctx = createContext(host, contextPath);
        //设置访问路径
        ctx.setPath(contextPath);
        //设置物理工作目录 
        ctx.setDocBase(docBase);
        //添加监听器
        ctx.addLifecycleListener(new DefaultWebXmlListener());
        ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
        //ContextConfig也同样实现了监听接口
        ctx.addLifecycleListener(config);

        // prevent it from looking ( if it finds one - it'll have dup error )
        config.setDefaultWebXml(noDefaultWebXmlPath());

        //将Servlet容器(Context)添加到Host下
        if (host == null) {
            getHost().addChild(ctx);
        } else {
            host.addChild(ctx);
        }

        return ctx;
    }

(2)Tomcat启动Tomcat.start()
Tomcat中启动中使用了观察者设计模式,所有容器实现了LifeCycle接口(也就是Observable),所有修改和状态变化由容器通知已注册的Observer(Listener)。

(3)Context容器初始化:
当Context容器初始化状态为init时,ContextConfig实现了LifeCycleListener接口,之前在addWebApp()已经将其注册到了Context中,这时会被调用。ContextConfig负责整个Web应用配置文件的解析工作,在ContextConfig.init()方法中(包括/conf目录下的context.xml,默认HOST配置文件/server.xml,Context自身的配置文件)。

(4)配置文件解析完成后调用Context.startInternal:这个方法十分重要,包含很多工作,之后要涉及的比如Web应用初始化servlet的创建初始化(loadOnStartup的),Filter的创建和初始化等等都是这个方法子环节:

创建读取资源文件的对象;
创建ClassLoader对象(WepAppClassLoader,加载Web应用目录lib下的jar包中的类,不同Web应用这里相互隔离);
设置应用的工作目录‘;
启动相关的辅助类(logger,realm,resources等);
修改启动状态,通知感兴趣的观察者;
子容器的初始化;
获取ServletContext并设置必要的参数;
创建并初始化Filter
初始化LoadOnStartup的Servlet

(5)Web应用初始化:
在上面说过Context.startInternal方法中会“修改启动状态,通知感兴趣的观察者”,查看该方法源码可以发现:

    fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null);

这个方法通知注册对于CONFIGURE_START_EVENT感兴趣的监听器,就包括ContextConfig,这时ContextConfig调用configureStart方法开始Web应用的初始化工作,主要的工作就是web.xml文件的解析(包括全局的web.xml,应用自己的web.xml,jar包中META-INF/web-fragment.xml,注解的读取,解析,合并)。

这些web.xml部署描述符和注解是依据Serlvet规范的,WebXml对象将它们抽象组装成StandardWapper,Tomcat容器内部的表示方法,而不是直接强耦合于Serlvet规范。

这个过程将我们熟悉的Serlvet,Filter,Multipart配置抽象包装成StandardWrapper对象,作为子容器添加到Context中,Context容器是真正运行Servlet的Servlet容器,一个Web应用一个Context容器。

7.2 创建Servlet实例

这个工作是在Context.startInternal()中开始的:

    if (ok) {
        if (!loadOnStartup(findChildren())) {
            log.error(sm.getString("standardContext.servletFail"));
            ok = false;
        }
    }

StandardContext.loadOnStartup对loadOnstartup值大于等于0的StandardWrapper调用其load方法,开始创建和初始化Servlet对象。

/conf/web.xml全局的部署描述符中定义两个Servlet:
org.apache.catalina.servlets.DefaultServletorg.apache.jasper.servlet.JspServlet(loadOnStartup分别是1和3);根据/conf/web.xml总的定义我们可以知道它们分别是处理静态资源和jsp文件请求的Servlet。

7.2.1 创建Servlet对象:Servlet创建中的单例模式(synchronized+反射创建)

之前在介绍Servlet规范时,我们提及了Servlet是单例的,这里就看看Tomcat是怎样支持这一规范要求的。

首先,根据前面的知识,已经知道我们在部署描述符中定义的每个Servlet会被解析组装成对应一个StandardWrapper对象,也正是这个对象负责创建Servlet;创建就在StandardWrapper.loadServlet方法中,下面来看看这个方法的一些关键步骤:

(1)基于synchronized同步控制,保证create-if-not的原子性/内存可见性:

    public synchronized Servlet loadServlet() throws ServletException

这里到没有DCL,静态内部类,枚举等丰富多彩的单例模式实现方法,其中也没有什么比较特别耗时的操作;但是这也说明了Serlvet使用LoadOnStartup可以避免在Web应用运行的时候因为创建Servlet的一些同步开销。

(2)如果单例已存在,直接返回:

// Nothing to do if we already have an          instance or an instance pool
        if (!singleThreadModel && (instance != null))
            return instance;

singleThreadModel(以下简称STM)前面已经说过使用对象池来保证不会有两个线程使用同一个Servlet实例(但这不是一个好办法)。

(3)获取InstatnceManager实例:

    InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();

(4)InstanceManager通过反射创建servlet实例:

            try {
                servlet = (Servlet) instanceManager.newInstance(servletClass);
            } catch (ClassCastException e) {
                /* 略 */
            } catch (Throwable e) {
                /* 略 */
            }

(5)开始初始化Servlet,初始化完成后通知执行回调:

    initServlet(servlet);
    fireContainerEvent("load", this);

7.2.2 初始化Servlet

在上面一小节的StandardWrapper.loadServlet的结尾开始进行Servlet的初始化工作(根据StandardWrapper的源码,initServlet方法一般会在loadServlet调用后,检查如果没有完成初始化就进行调用,因为loadServlet可能因为一些异常,比如UnavailableException等原因中途退出而没有完成初始化)。

initServlet方法中有大量的回调事件通知:

    InstanceEvent.BEFORE_INIT_EVENT
    /* servlet.init(facade)*/
    InstanceEvent.AFTER_INIT_EVENT

(1)基于synchronized关键字的同步控制:

    private synchronized void initServlet(Servlet servlet)
            throws ServletException

(2)如果已经初始化过了或者不是STM模式直接返回:

    if (instanceInitialized && !singleThreadModel) return;

(3)将StandardWrapper包装成StandardWrapperFacade作为ServletConfig传给Servlet,调用Servlet.init(facade)

            if( Globals.IS_SECURITY_ENABLED) {
                /* 略:SecurityUtil.doAsPrivilege方法调用 */
            } else {
                servlet.init(facade);
            }
            instanceInitialized = true;

(4)GenericServlet(HttpServlet,JspServlet基类)的init注入保存了ServletConfig对象。
如果Servlet是JspServlet,需要编译这个JSP文件为类,并初始化这个类。

7.3 Serlvet核心结构和门面设计模式

Servet直接相关的几个类:ServletConfig,ServletReuqest,ServletResponse。

Tomcat容器中使用内部的表示方法,通过门面设计模式将Facade对象传递给Servlet:
Tomcat-Servlet体系结构
从这个类图中,我们可以看到ServletConfig和ServletContext与Servlet的关系,以及Tomcat对它们的实现:
(1)一个ServletContext对应多个Servlet,Tomcat中的实现类型是ApplicationContext,ApplicationContextFacade是它的门面类;
(2)一个ServletConfig对应一个Servlet,Tomcat中的实现类型是StandardWrapper,StandardWrapperFacade是其门面类;
(3)ServletConfig是Servlet配置集合,ServletContext是容器内所有Servlet的“交易环境”;
(4)Servlet桶构init方法获取ServletConfig(实际上是StandardWrapperFacade对象);
(5)ApplicationContext和StandardWrapper都是在StandardContext中创建的。

RequestResponse
Tomcat-Request-Response体系结构
Tomcat同样使用内部表示<—>门面类<—>传入Servlet;

(1)Tomcat接收到请求后创建org.apache.coyote.Requestorg.apache.coyote.Resposne,这两个类是轻量级的类,对象很小;这是有Tomcat内部工作线程创建的;
(2)将org.apache.coyote.Requestorg.apache.coyote.Resposne传递给用户线程,创建org.apache.catalina.connector.Requestorg.apache.catalina.connector.Resposne,这两个对象一直整个Servlet容器直到要传给Servlet;
(3)创建门面类RequestFacadeResponseFacade给Servlet;

7.3 请求和映射/分派

Tomcat8通过org.apache.catalina.mapper(和Tomcat7位置有差别)保存容器中所有子容器的信息,在org.apache.catalina.connector.Request进入Container容器前,Mapper会根据这次请求的hostname和contextPath将host和context容器设置到Request的mappingData属性中。

这里同样使用观察者模式,MappingListener注册到Engine,Host各级容器上,容器状态发生变化就通知它变化更新到Mapper中。

根据Mapper可以确定将请求分派到哪个Host和哪个Servlet容器上以及哪个Servlet上,在传到Servlet前,通过Filter链并在这个过程中调用可能的Listener,最终执行Servlet的service方法。
Tomcat-Mapper结构

7.4 Listener的体系结构和创建

Servlet规范中定义了很多监听器,基于观察者模式将主要流程的控制/管理和事件的响应处理分离。主要分为两类:
(1)LifeCycleListener:ServletContextListener,HttpSessionListener;监听目标对象的创建和销毁事件;
(2)EventListener:ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener,HttpSessionAttributeListener等等;

PS:ServletContextListeer在容器启动之后不能在添加新的,因为容器启动这个事件不会再次发生;我们可以在ServletContainerInitializer中创建配置它。

Listener的创建

再次回到StandardContext.startInternal方法中:

    if (ok) {
        if (!listenerStart()) {
           log.error(sm.getString("standardContext.listenerFail"));
           ok = false;
        }
    }

这里的关键还是listenerStart方法,该方法在StandardContext.filterStart之前,我们来看一看这个方法的关键步骤:
(1)反射创建所有Listener:

// Instantiate the required listeners
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i < results.length; i++) {
    /* 略 */
    try {
        String listener = listeners[i];
        results[i] = getInstanceManager().newInstance(listener);
    } catch (Throwable t) {
        /* 略 */
        ok = false;
    }
}

(2)将监听器整理为eventListeners和lifecycleListeners两类:

// Sort listeners in two arrays
ArrayList<Object> eventListeners = new ArrayList<>();
ArrayList<Object> lifecycleListeners = new ArrayList<>();
for (int i = 0; i < results.length; i++) {
    if ((results[i] instanceof ServletContextAttributeListener)
        || (results[i] instanceof ServletRequestAttributeListener)
        || (results[i] instanceof ServletRequestListener)
        || (results[i] instanceof HttpSessionIdListener)
        || (results[i] instanceof HttpSessionAttributeListener)) {
        eventListeners.add(results[i]);
    }
    if ((results[i] instanceof ServletContextListener)
        || (results[i] instanceof HttpSessionListener)) {
        lifecycleListeners.add(results[i]);
    }
}

(3)将Intializers或其他通过编程方式添加的监听添加到位:
这里就不一定是反射创建的了,在ServletContainerInitializer.onStratup中我们可以通过构造器来创建指定的listener;

// Listener instances may have been added directly to this Context by
        // ServletContextInitializers and other code via the pluggability APIs.
        // Put them these listeners after the ones defined in web.xml and/or
        // annotations then overwrite the list of instances with the new, full
        // list.
        for (Object eventListener: getApplicationEventListeners()) {
            eventListeners.add(eventListener);
        }
        setApplicationEventListeners(eventListeners.toArray());
        for (Object lifecycleListener: getApplicationLifecycleListeners()) {
            lifecycleListeners.add(lifecycleListener);
            if (lifecycleListener instanceof ServletContextListener) {
                noPluggabilityListeners.add(lifecycleListener);
            }
        }
        setApplicationLifecycleListeners(lifecycleListeners.toArray());

(4)调用ServletContextListener的contextInitialized

7.5 Filter的创建,初始化和使用

Filter的创建

回到StandardContext.startInternal方法中:

        // Configure and call application filters
        if (ok) {
            if (!filterStart()) {
                log.error(sm.getString("standardContext.filterFail"));
                ok = false;
            }
        }

StandardContext.filterStart中将根据配置创建所有的ApplicationFilterConfig以及根据FilterClass反射创建爱Filter实例,实际上还是通过(synchronized+反射创建保证单例),该方法在StandardContext.loadOnStartup之前调用。

Filter链的结构和调用过程

上面我们根据Servlet规范介绍了Filter的基本情况。这里结合Tomcat 8具体介绍下Filter的创建,初始和相关细节。
Tomcat-Filter结构

Tomcat容器主要通过ApplicationFilterChain管理和执行过滤器链。它通过一个数组保存所有Filter的FilterConfig对象,在Tomcat中是ApplicationFilterConfig(每个FilterConfig包含一个Filter引用)。

    private ApplicationFilterConfig[] filters =
        new ApplicationFilterConfig[0];

该数组是一个大小动态增长的数组(每次增长10)。处理请求时通过ApplicationFilterChain.doFilter该方法会调用数组中每个Filter.doFilter

7.6 Initializer,Listener,Filter,Servlet的创建/初始化,销毁顺序

Listener,Filter,Servlet的创建和初始化上面已经结合Tomcat 8的实现进行了总结说明。它们都是在StandardContext.startInternal这一生命周期方法中进行的。加上Initializer顺序依次是:
Initaializer—>Listener—>Filter—>Servlet(loadOnstart);

因为前面没有提及Intializer的相关知识,我们在这里介绍下:
ServletContainInitalizer是Java EE 6中Servlet 3.0的新增接口;它的onStartup方法是一个web应用中我们的代码可以控制到的最早时间点。

它不需要通过web.xml部署描述符来定义,需要在/META-INF/services/javax.servlet.ServletContainerInitializer中列出具体的实现,Servlet容器在启动时会自动扫描加载它们并调用onStartUp方法。但是文件不能放在WAR文件的/META-INF/services中,而是需要放在JAR文件的/META-INF/services中,这样就很不方便。如果你使用Spring的话,Spring Framework提供了一个桥接口,在Spring中SpringServletContainerInitializer类实现了ServletContainerInitializer接口,Spring的JAR中列出了SpringServletContainerInitializer,如下。在SpringServletContainerInitializer中会扫描所有WebApplicationInitializer的实现,调用它们的onStartUp方法,因此我们不必在劳神费心了。

Initializer的调用

StandardContext.startInternal中在lislistener,filter,servlet(loadOnStartup)之前对所有的ServletContainerInitializers进行调用:

// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
    initializers.entrySet()) {
    try {
        entry.getKey().onStartup(entry.getValue(),
                getServletContext());
    } catch (ServletException e) {
        log.error(sm.getString("standardContext.sciFail"), e);
        ok = false;
        break;
    }
}

因此,我们可以看到,根据规范结合实现,Initializer中可以配置servlets,filters和listeners;在ServletContextListener可以配置其他的listener(因此listenerStart中分了两步加载),filters和servlets;而Filter链在Servlet之前调用。因此我们能看到这样一个顺序:
Initaializer—>Listener—>Filter—>Servlet(loadOnstart);

销毁顺序

和C++构造&析构等等这类东西很相似,它们的销毁顺序和加载/初始化顺序是相反的。

结合Servlet规范中一些定义,我们也能看到上述初始化和销毁顺序,这也是必须要理解明白的重要知识点。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐