学习参考资料:
(1)Servet 3.1 final 规范;
(2)《Java Web高级编程》;
(3)《深入分析Java Web技术内幕》(第2版);
心得:虽然现在是实际工作中很少直接使用Servlet,但了解Servlet规范中对不同组件(Servlet,Filter,Listener等等)以及Servlet容器的实现对于基于Servlet的Java EE应用的理解也是大有益处的。因此基于上面3个资料的学习所得以及我自己阅读Tomcat 8相关部分源码的一些收获在这里总结记录一下。
Servlet 容器是 web server 或 application server 的一部分,提供基于请求/响应发送模型的网络服务,解码基于 MIME 的请求,并且格式化基于 MIME 的响应。Servlet 容器也包含了管理 Servlet 生命周期。
Servlet 是基于 Java 技术的 web 组件,被容器所托管的,用于生成动态内容。像其他基于 Java 的组件技术一样,Servlet 也是基于平台无关的 Java 类格式,被编译为平台无关的字节码,可以被基于 Java 技术的 web server 动态加载并运行。
Servlet默认是线程不安全的,一个容器中只有每个servlet一个实例,但是如果实现了SingleThreadModule
接口,容器将实现多个servlet实例
SingleThreadModule
也不能保证线程安全,它只能保证任意两个线程不会使用同一个Servlet实例(可能由一个对象池来维护),servlet2.4已经将这个接口已经标注为已过时了;
我查看了Tomcat 8.0中StandardWrapper源码,这个类负责Servlet的创建,其中SingleThreadModule模式下创建的实例数不能超过20个,也就是同时只能支持20个线程访问这个Serlvet,因此,这种对象池的设计会进一步限制并发能力和可伸缩性。
加载和实例化: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方法是是否必要的。
Serlvet和Filter有三种不同的匹配规则:
(1)精确匹配:/foo;
(2)路径匹配:/foo/*;
(3)后缀匹配:*.html;
Serlvet的匹配顺序是:
首先进行精确匹配;如果不存在精确匹配的进行路径匹配;最后根据后缀进行匹配;一次请求只会匹配一个Servlet;(Filter是只要匹配成功就添加到FilterChain)
PS:其他写法(/foo/,/*.html,*/foo)都不对;“/foo*”不能匹配/foo,/foox;
通过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数据(因为已经读取过了);
数据以multipart/form-data格式发送,servlet支持文件上传;
通过 HttpServletRequest
的:
public Collection
;
public Part getPart(String name)
;
每个Part类代表从multipart/form-data格式的POST请求中接受的一个部分或表单项,每个Part可以通过Part.getInputStream
方法访问头部,内容类型和内容;
对于表单数据的Content-Disposition,即使没有文件名,也可使用part的名称通过HttpServletRequest的getParameter和getParameterValues得到part的字符串值;
属性的作用域与请求相关;
getAttribute/getAttributeNames/setAttribute;
对于这样的请求各个部分是怎样的: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 URL:http://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”是项目根目录名;这两个方法都是基于项目根目录返回的;
非阻塞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);
getParameter等参数获取方法会将参数部分从流中读取出来,因此一定要在getParameter调用前设置编解码方式:
Request:
setCharacterEncoding()
;
Response:
setCharacterEncoding()
;
setHead()
;
setContentType()
;
下面在Response总结中会进一步说明编码和响应及其缓冲区之间的关系.
每个Request对象在Servlet的service(这就包括JSP的表达式,脚本,声明),Filter的doFilter的作用域中有效;
启用了异步处理后,request对象将到AsyncContext的complete调用时;
每个基于Servlet的Web应用都有自己的ServletContext保存和维护自己的上下文信息,包括:初始化参数,Servlet,Filter,Listener配置,容器属性等等。
主要有3种方式:
(1)Web.xml部署描述符;
(2)注解;
(3)通过ServletContextListener/ServletContainerInitializer使用Servlet/Filter的Registration配置;
容器也有自己的属性,这里提一下是因为这涉及到:
(1)EL表达式的隐式变量及作用域:applicationScope包含所有绑定到ServletContext的特性;EL表达式中变量的作用域也是一层层查找的,最后一层查找范围就是ServletContext的特性;
(2)同样JSP中的隐式变量application也是ServletContext实例;
获取Web应用下的资源:
getResource
和getResourceAsStream
;
传入path,必须要以“/”开头,相对与两个目录:上下文的根目录和web应用的WEB-INF/lib中的JAR文件中的META-INF/resources目录。依次查找这两个地方;
这两个方法不能获取动态内容,比如jsp,获取的是jsp文件源码而不是处理后的响应;
Servlet容器必须为每一个servlet上下文提供一个私有的临时目录,并将通过javax.servlet.context.tempdir上下文属性使其可用,该属性关联的是java.io.File。
这个目录也是Multipart处理中临时目录的默认目录,并且location如果是相对路径也是基于它的。
Response的
getWriter
和getOutputStream
在同一次请求中不能同时被调用。调用了一个之后在调用另一个会抛出IllegalStateException;
获取和设置缓冲区大小:getBufferSize和setBufferSize,不能在缓冲区写入内容之后设置缓冲区大小调用setBufferSize;
PS:tomcat 8中缓冲区大小为8192
是否提交到客户端:isCommitted;
刷新缓冲区:flushBuffer,也可以通过getWriter/getOutputStream调用输出流的flush;
重置缓冲区:reset和resetBuffer,不能在响应提交后调用,否则抛出IllegalStateException,响应及关联的缓冲区不变;
PS:一般并不需要进行手动刷新缓冲区,service方法结束或请求处理完成后,容器会自动刷新缓冲区.但如果使用异步处理分派的话,Response的生命周期其实已经延伸到了开始异步的service方法之外了,这样如果你想要在service方法返回前提交响应则可以手动刷新缓冲区,否则只能等到异步完成/超时请求处理结束或者缓冲区满了才能提交到客户端了.
sendRedirect
和sendError
;
这两方法有一些相似性:
(1)如果在调用前已有响应提交到客户端,调用它们将抛出IllegalStateException;
(2)如果没有响应提交,sendRedirect和sendError将重置缓冲区,舍弃原来缓冲区中的旧数据,Servlet中之后的输出也是无效的(将被忽略);
同样需要在响应未提交或resp.getWriter()之前进行设置,否则将无效(面向字符的输出已经设置默认编码);
国际化配置
在部署描述符中配置,如果没有配置将使用容器依赖的mapping等配置:
<locale-encoding-mapping-list>
<locale-encoding-mapping>
<locale>zh_CNlocale>
<encoding>UTF8encoding>
locale-encoding-mapping>
locale-encoding-mapping-list>
setLocale也可以设置编码,在setContentType
和setCharacterEncoding
之前,调用setLocale
设置编码,使用上面配置中的编码;但是这并不会设置HTTP响应头的content-type等头,因此浏览器/客户端将使用默认的解码方式来解码这可能导致乱码;
PS:setLocale将通过Content-Language
响应头来传递;但是编码方式如果没有指定Content-Type,是不能通过HTTP header传递的;
因此,应该在getWriter方法被调用或响应被提交之前通过setContentType
或setCharacterEncoding
或addHeader
设置编码方式,否则将使用默认编码:ISO-8859-1;
setCharacterEncoding:这个方法可以覆盖setLocale
和setContentType
设置的编码方式,但不会设置Content-Type头;
setLocale
,setCaracterEncoding
和setContentType
都可以设置编码方式,但是要通过setContentType
和addHeader
设置Content-Type
响应头,并且它们都要在getWriter
调用前或响应提交前设置;
以下时间表明servlet满足了请求且响应对象即将关闭“
(1)servlet的service方法终止;
(2)响应的setContentLength
或setContentLong
制定了大于零的内容量,且已经写入到响应;
(3)sendError
方法或sendRedirect
方法已调用;
(4)AsyncContext的compelete
方法已调用;
setContentLength
和setContentLengthLong
方法一般有Web容器在响应完成后负责调用,后者是Servlet3.1的新方法;
和Request相似,在servlet的service方法和Filter的doFilter方法内有效,如果启动异步处理,直到complete方法被调用有效。
Filter和Servlet/其他Web资源(包括静态资源)组合起来使用,实现了一个职责链模式的请求处理调用栈,Servlet/Web资源是最后一个“入栈的节点”(当然Filter可以阻止请求到达Servlet/Web资源,)。Filter可以在servlet调用前和调用后进行一些额外的处理过程(比如,验证,日志,压缩等等)。
FilterChain.doFilter(req, resp)
调用前后,正分别是调用栈“入栈”和“出栈”之时,做相应的处理。每个Filter配置对应的每个JVM的容器仅实例化一个实例。
(1)init()/init(FilterConfig filterConfig)
:和Servlet一样可能抛出UnavaliableException(暂时不可用/永久不可用);init()
方法总是在应用程序启动时调用(ServletContextListener初始化之后,Servlet初始化之前);
(2)doFilter()
:服务中,处理传入请求和返回响应:可以进行检查请求头,修改请求头/数据,修改响应头等等;
Filter可以调用chain.doFilter()方法调用过滤器链中下一个实体;也可以不调用来阻止请求;
doFilter过程中也可能抛出UnavailableException,容器负责停止处理剩下的过滤器链,若不是永久不可用,可以选择稍后重试整个链。
(3)destroy()
:容器把服务中的Filter实例移除前,先调用它的destroy方法,进行释放资源等清理工作;
Servlet容器中,存在多种分派方式,Servlet2.4之后,可以对不同的请求分派进行过滤:
(1)普通请求;
(2)转发请求:RequestDispatcher.forward()
或
触发的请求,这种转发,本质上是服务器应用内部的方法调用;
(3)包含请求:RequestDispatcher.include()
或
,注意这种是包含输出和<%@ inlcude %>
静态导入的区别;
(4)错误资源请求:发生异常,请求错误页面;
(5)异步请求:如果要结合异步处理的Servlet使用,Filter同样也要开启支持异步处理。这里异步请求指的是有AsyncContext
派发的请求,实现异步过滤器要注意可能被单个异步请求调用多次(潜在的多个不同线程);
在部署描述符中,通过
元素中可以选择Filter支持的请求类型。
初始化参数的设置;
同样Filter也可以通过编程,注解和XML三种方式配置;
到Serlvet 3.1,注解配置Filter不能保证设置顺序。
Filter的顺序:
(1)基本顺序:
首先,
匹配,按照Filter在部署描述符中出现的顺序匹配过滤器映射;
其次再按照
出现的顺序匹配;
(2)编程设置Filter顺序:
registration.addMappingForUrlPatterns(null, false, "/foo", "bar/*");
第二个参数表示是否在部署描述符之后的Filter之后;
UnavailableException
表示Servlet或Filter不可用,这种情况一般Servlet容器负责处理,重试或者返回响应;
UnavailableException的分为两种:
(1)永久不可用:比如servlet配置不正确或者Filter状态异常。
(2)暂时不可用:可能由于一些system-wide的问题导致请求无法处理,比如第三方服务不可用,内存和磁盘不足等等;可以在稍后重试;
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" />
这里的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容器。
这个工作是在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.DefaultServlet
和org.apache.jasper.servlet.JspServlet
(loadOnStartup分别是1和3);根据/conf/web.xml总的定义我们可以知道它们分别是处理静态资源和jsp文件请求的Servlet。
之前在介绍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);
在上面一小节的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文件为类,并初始化这个类。
Servet直接相关的几个类:ServletConfig,ServletReuqest,ServletResponse。
Tomcat容器中使用内部的表示方法,通过门面设计模式将Facade对象传递给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中创建的。
Request和Response:
Tomcat同样使用内部表示<—>门面类<—>传入Servlet;
(1)Tomcat接收到请求后创建org.apache.coyote.Request
和org.apache.coyote.Resposne
,这两个类是轻量级的类,对象很小;这是有Tomcat内部工作线程创建的;
(2)将org.apache.coyote.Request
和org.apache.coyote.Resposne
传递给用户线程,创建org.apache.catalina.connector.Request
和org.apache.catalina.connector.Resposne
,这两个对象一直整个Servlet容器直到要传给Servlet;
(3)创建门面类RequestFacade
和ResponseFacade
给Servlet;
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方法。
Servlet规范中定义了很多监听器,基于观察者模式将主要流程的控制/管理和事件的响应处理分离。主要分为两类:
(1)LifeCycleListener:ServletContextListener,HttpSessionListener;监听目标对象的创建和销毁事件;
(2)EventListener:ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener,HttpSessionAttributeListener等等;
PS:ServletContextListeer在容器启动之后不能在添加新的,因为容器启动这个事件不会再次发生;我们可以在ServletContainerInitializer中创建配置它。
再次回到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
(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
;
回到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
之前调用。
上面我们根据Servlet规范介绍了Filter的基本情况。这里结合Tomcat 8具体介绍下Filter的创建,初始和相关细节。
Tomcat容器主要通过ApplicationFilterChain
管理和执行过滤器链。它通过一个数组保存所有Filter的FilterConfig对象,在Tomcat中是ApplicationFilterConfig
(每个FilterConfig包含一个Filter引用)。
private ApplicationFilterConfig[] filters =
new ApplicationFilterConfig[0];
该数组是一个大小动态增长的数组(每次增长10)。处理请求时通过ApplicationFilterChain.doFilter
该方法会调用数组中每个Filter.doFilter
。
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方法,因此我们不必在劳神费心了。
StandardContext.startInternal
中在lislistener,filter,servlet(loadOnStartup)之前对所有的ServletContainerInitializers进行调用:
// Call ServletContainerInitializers
for (Map.Entry>> 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规范中一些定义,我们也能看到上述初始化和销毁顺序,这也是必须要理解明白的重要知识点。