怎么理解ServletContext,在Servlethttp协议访问过程程中什么作用

在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
最近学spring,碰到这个问题。
DispatcherServlet:一个web应用可以有多个 DispatcherServlet 。我想知道每个 DispatcherServlet 对应一个 WebApplicationContext 吗?
我知道 ServletContext 是一个web应用中只有一个,并且 WebApplicatioContext 被放在 ServletContext 中。
书上说:WebApplicationContext在ServletContext 中的 KEY 是WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,我们如果要使用
WebApplicationContext 则需要从 ServletContext 取出。
也就是说一个 ServletContext 中只包含一个 WebApplicationContext 吗?
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
要想很好理解这三个上下文的关系,需要先熟悉spring是怎样在web容器中启动起来的。spring的启动过程其实就是其IoC容器的启动过程,对于web程序,IoC容器启动过程即是建立上下文的过程。
spring的启动过程:
首先,对于一个web应用,其部署在web容器中,web容器提供其一个全局的上下文环境,这个上下文就是ServletContext,其为后面的spring IoC容器提供宿主环境;
其次,在web.xml中会提供有contextLoaderListener。在web容器启动时,会触发容器初始化事件,此时contextLoaderListener会监听到这个事件,其contextInitialized方法会被调用,在这个方法中,spring会初始化一个启动上下文,这个上下文被称为根上下文,即WebApplicationContext,这是一个接口类,确切的说,其实际的实现类是XmlWebApplicationContext。这个就是spring的IoC容器,其对应的Bean定义的配置由web.xml中的context-param标签指定。在这个IoC容器初始化完毕后,spring以WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE为属性Key,将其存储到ServletContext中,便于获取;
再次,contextLoaderListener监听器初始化完毕后,开始初始化web.xml中配置的Servlet,这个servlet可以配置多个,以最常见的DispatcherServlet为例,这个servlet实际上是一个标准的前端控制器,用以转发、匹配、处理每个servlet请求。DispatcherServlet上下文在初始化的时候会建立自己的IoC上下文,用以持有spring mvc相关的bean。在建立DispatcherServlet自己的IoC上下文时,会利用WebApplicationContext.ROOTWEBAPPLICATIONCONTEXTATTRIBUTE先从ServletContext中获取之前的根上下文(即WebApplicationContext)作为自己上下文的parent上下文。有了这个parent上下文之后,再初始化自己持有的上下文。这个DispatcherServlet初始化自己上下文的工作在其initStrategies方法中可以看到,大概的工作就是初始化处理器映射、视图解析等。这个servlet自己持有的上下文默认实现类也是mlWebApplicationContext。初始化完毕后,spring以与servlet的名字相关(此处不是简单的以servlet名为Key,而是通过一些转换,具体可自行查看源码)的属性为属性Key,也将其存到ServletContext中,以便后续使用。这样每个servlet就持有自己的上下文,即拥有自己独立的bean空间,同时各个servlet共享相同的bean,即根上下文(第2步中初始化的上下文)定义的那些bean。
说完了spring上下文的初始化过程,这三个上下文的关系应该就了解了。如还是不太清楚,我就爱莫能助了,只能自行看代码去了。
同步到新浪微博
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:4378人阅读
Java web(8)
学习参考资料:
(1)Servet 3.1 final 规范;
(2)《Java Web高级编程》;
心得:虽然现在是实际工作中很少直接使用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);
完成:plete();必须在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调用plete()或AsyncContext.dispatch(),执行一个状态码为HttpServletResponse
.SC_INTERNAL_SERVER_ERROR出错分派;
(3)如果没有找到错误页面或者错误页面没有调用plete()/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 请求路径元素
对于这样的请求各个部分是怎样的:空幻?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:空幻;
路径转换方法
ServletContext.getRealPath;
HttpServletRequest.getPathTranslated;
(1)“”, 在我的机器上`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调用前设置编解码方式:
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配置,容器属性等等。
主要有3种方式:
(1)Web.xml部署描述符;
(2)注解;
(3)通过ServletContextListener/ServletContainerInitializer使用Servlet/Filter的Registration配置;
4.2 上下文属性
容器也有自己的属性,这里提一下是因为这涉及到:
(1)EL表达式的隐式变量及作用域:applicationScope包含所有绑定到ServletContext的特性;EL表达式中变量的作用域也是一层层查找的,最后一层查找范围就是ServletContext的特性;
(2)同样JSP中的隐式变量application也是ServletContext实例;
获取Web应用下的资源:
getResource和getResourceAsStream;
传入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的getWriter和getOutputStream在同一次请求中不能同时被调用。调用了一个之后在调用另一个会抛出IllegalStateException;
获取和设置缓冲区大小:getBufferSize和setBufferSize,不能在缓冲区写入内容之后设置缓冲区大小调用setBufferSize;
PS:tomcat 8中缓冲区大小为8192
是否提交到客户端:isCommitted;
刷新缓冲区:flushBuffer,也可以通过getWriter/getOutputStream调用输出流的flush;
重置缓冲区:reset和resetBuffer,不能在响应提交后调用,否则抛出IllegalStateException,响应及关联的缓冲区不变;
PS:一般并不需要进行手动刷新缓冲区,service方法结束或请求处理完成后,容器会自动刷新缓冲区.但如果使用异步处理分派的话,Response的生命周期其实已经延伸到了开始异步的service方法之外了,这样如果你想要在service方法返回前提交响应则可以手动刷新缓冲区,否则只能等到异步完成/超时请求处理结束或者缓冲区满了才能提交到客户端了.
5.2 重定向和设置Error
sendRedirect和sendError;
这两方法有一些相似性:
(1)如果在调用前已有响应提交到客户端,调用它们将抛出IllegalStateException;
(2)如果没有响应提交,sendRedirect和sendError将重置缓冲区,舍弃原来缓冲区中的旧数据,Servlet中之后的输出也是无效的(将被忽略);
5.3 Response编码和国际化
同样需要在响应未提交或resp.getWriter()之前进行设置,否则将无效(面向字符的输出已经设置默认编码);
国际化配置
在部署描述符中配置,如果没有配置将使用容器依赖的mapping等配置:
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调用前或响应提交前设置;
5.4 结束响应对象
以下时间表明servlet满足了请求且响应对象即将关闭“
(1)servlet的service方法终止;
(2)响应的setContentLength或setContentLong制定了大于零的内容量,且已经写入到响应;
(3)sendError方法或sendRedirect方法已调用;
(4)AsyncContext的compelete方法已调用;
setContentLength和setContentLengthLong方法一般有Web容器在响应完成后负责调用,后者是Servlet3.1的新方法;
5.5 Response生命周期
和Request相似,在servlet的service方法和Filter的doFilter方法内有效,如果启动异步处理,直到complete方法被调用有效。
6.1 对Filter的理解
Filter和Servlet/其他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之后;
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工程:
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));
ctx.addLifecycleListener(config);
config.setDefaultWebXml(noDefaultWebXmlPath());
if (host == null) {
getHost().addChild(ctx);
host.addChild(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 (!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。
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)如果单例已存在,直接返回:
if (!singleThreadModel && (instance != null))
singleThreadModel(以下简称STM)前面已经说过使用对象池来保证不会有两个线程使用同一个Servlet实例(但这不是一个好办法)。
(3)获取InstatnceManager实例:
InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
(4)InstanceManager通过反射创建servlet实例:
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
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) {
servlet.init(facade);
instanceInitialized = true;
(4)GenericServlet(HttpServlet,JspServlet基类)的init注入保存了ServletConfig对象。
如果Servlet是JspServlet,需要编译这个JSP文件为类,并初始化这个类。
7.3 Serlvet核心结构和门面设计模式
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;
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方法。
7.4 Listener的体系结构和创建
Servlet规范中定义了很多监听器,基于观察者模式将主要流程的控制/管理和事件的响应处理分离。主要分为两类:
(1)LifeCycleListener:ServletContextListener,HttpSessionListener;监听目标对象的创建和销毁事件;
(2)EventListener:ServletContextAttributeListener,ServletRequestAttributeListener,ServletRequestListener,HttpSessionAttributeListener等等;
PS:ServletContextListeer在容器启动之后不能在添加新的,因为容器启动这个事件不会再次发生;我们可以在ServletContainerInitializer中创建配置它。
Listener的创建
再次回到StandardContext.startInternal方法中:
if (!listenerStart()) {
log.error(sm.getString("standardContext.listenerFail"));
ok = false;
这里的关键还是listenerStart方法,该方法在StandardContext.filterStart之前,我们来看一看这个方法的关键步骤:
(1)反射创建所有Listener:
String listeners[] = findApplicationListeners();
Object results[] = new Object[listeners.length];
boolean ok = true;
for (int i = 0; i & results. i++) {
String listener = listeners[i];
results[i] = getInstanceManager().newInstance(listener);
} catch (Throwable t) {
ok = false;
(2)将监听器整理为eventListeners和lifecycleListeners两类:
ArrayList&Object& eventListeners = new ArrayList&&();
ArrayList&Object& lifecycleListeners = new ArrayList&&();
for (int i = 0; i & results. 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;
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方法中:
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容器主要通过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进行调用:
for (Map.Entry&ServletContainerInitializer, Set&Class&?&&& entry :
initializers.entrySet()) {
entry.getKey().onStartup(entry.getValue(),
getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
因此,我们可以看到,根据规范结合实现,Initializer中可以配置servlets,filters和listeners;在ServletContextListener可以配置其他的listener(因此listenerStart中分了两步加载),filters和servlets;而Filter链在Servlet之前调用。因此我们能看到这样一个顺序:
Initaializer—&Listener—&Filter—&Servlet(loadOnstart);
和C++构造&析构等等这类东西很相似,它们的销毁顺序和加载/初始化顺序是相反的。
结合Servlet规范中一些定义,我们也能看到上述初始化和销毁顺序,这也是必须要理解明白的重要知识点。
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:74941次
积分:1187
积分:1187
排名:千里之外
原创:40篇
评论:18条
(3)(1)(1)(3)(6)(7)(6)(6)(1)(3)(1)(6)
(window.slotbydup = window.slotbydup || []).push({
id: '4740887',
container: s,
size: '250,250',
display: 'inlay-fix'

我要回帖

更多关于 浏览器访问服务器过程 的文章

 

随机推荐