Servlet详解(下)

前言

​ 在这一篇文章里,将会讨论ServletContext以及Servlet映射规则。这两个知识点非常重要,ServletContext直接关系到SpringIOC容器的初始化(请参考ContextLoaderListener解析),而Servlet映射规则与SpringMVC关系密切。

​ 可以说,作为初学者只要把这两点搞清楚,那么对Spring/SpringMVC的理解将会超过70%的程序员。我没开玩笑,你随便抓一个身边的同事问问,Tomcat和Spring什么关系?SpringMVC和Servlet什么关系?估计没几个能给你讲清楚的。

​ 我会先把SpringMVC讲得很简单,等大家觉得它就是个Servlet的时候,我又会把SpringMVC慢慢展开,露出它的全貌。此时你又会发现:SpringMVC is not only a Servlet.

主要内容:

  • ServletContext是什么
  • 如何获取ServletContext
  • Filter拦截方式之:REQUEST/FORWARD/INCLUDE/ERROR
  • Servlet映射器
  • 自定义DispatcherServlet
  • DispatcherServlet与SpringMVC
  • conf/web.xml与应用的web.xml

ServletContext是什么

ServletContext,直译的话叫做“Servlet上下文”,听着挺别扭。它其实就是个大容器,是个map。服务器会为每个应用创建一个ServletContext对象:

  • ServletContext对象的创建是在服务器启动时完成的
  • ServletContext对象的销毁是在服务器关闭时完成的

v2-40ed984999cab23bc4e9e17a39e84839_r

ServletContext对象的作用是在整个Web应用的动态资源(Servlet/JSP)之间共享数据。例如在AServlet中向ServletContext对象保存一个值,然后在BServlet中就可以获取这个值。

v2-291c4b8583663764b091fa2acd37e724_720w

这种用来装载共享数据的对象,在JavaWeb中共有4个,而且更习惯被成为“域对象”:

  • ServletContext域(Servlet间共享数据)
  • Session域(一次会话间共享数据,也可以理解为多次请求间共享数据)
  • Request域(同一次请求共享数据)
  • Page域(JSP页面内共享数据)

它们都可以看做是map,都有getAttribute()/setAttribute()方法。

来看一下物理磁盘中的配置文件与内存对象之间的映射关系

v2-2530b17c1ee7e94bbcce7ca472d6a667_r

每一个动态web工程,都应该在WEB-INF下创建一个web.xml,它代表当前整个应用。Tomcat会根据这个配置文件创建ServletContext对象

如何获取ServletContext

还记得GenericServlet吗?它在init方法中,将Tomcat传入的ServletConfig对象的作用域由局部变量(方法内使用)提升到成员变量。并且新建了一个getServletContext():

v2-e4fc1f0a49a67b59256b4935a47ac913_r

getServletContext()内部其实就是config.getServletContext()

也就是说ServletConfig对象可以得到ServletContext对象。但是这并不意味这ServletConfig对象包含着ServletContext对象,而是ServletConfig维系着ServletContext的引用。

其实这也很好理解:servletConfig是servletContext的一部分,就像他儿子。你问它父亲是谁,它当然能告诉你。

另外,Session域和Request域也可以得到ServletContext

session.getServletContext();
request.getServletContext();

v2-5b2c02d7dac0cd170c0194679f4d9483_720w

用域对象获得域对象

最后,还有个冷门的,我在监听器那一篇讲过了:

v2-ec7e00121cdd1df01898f9b91d27c60d_r

事件对象就是对事件源(被监听对象)的简单包装

所以,获取ServletContext的方法共5种(page域这里不考虑,JSP太少用了):

  • ServletConfig#getServletContext();
  • GenericServlet#getServletContext();
  • HttpSession#getServletContext();
  • HttpServletRequest#getServletContext();
  • ServletContextEvent#getServletContext();

Filter拦截方式之:REQUEST/FORWARD/INCLUDE/ERROR

在很多人眼里,Filter只能拦截Request:

v2-b8dfca0f5a4895bce75c2ce6b6f0c725_r

这样的理解还是太片面了。

其实配置Filter时可以设置4种拦截方式:

v2-bbd582e840cac5e82eda33bd91cf60bd_r

很多人要么不知道,要么理解得不够清晰。

这里,我先帮大家把这4种方式和重定向(Redirect)剥离开,免得有人搞混,它们是完全两类(前者和Request有关,后者通过Response发起),以FORWARD为例:

v2-70f251139437bb33e2f7a93dfa89c135_r

蓝色:重定向,橙色:转发

我们日常开发中,FORWARD用的最多的场景就是转发给JSP,然后模板输出HTML。

Redirect和REQUEST/FORWARD/INCLUDE/ERROR最大区别在于:

  • 重定向会导致浏览器发送2次请求,FORWARD们是服务器内部的1次请求

了解这个区别之后,我提一个很奇怪的问题:为什么这4种只引发1次请求?

是不是听傻了?我接下来给的答案,属于意料之外情理之中的那种:

因为FORWARD/INCLUDE等请求的分发是服务器内部的流程,不涉及浏览器

还记得如何转发吗:

v2-425f3e0e15abf4743546fff21c5d9999_r

我们发现通过Request或者ServletContext都可以得到分发器Dispatcher,但由于ServletContext代表整个应用,我更倾向于认为:ServletContext拥有分发器,Request是找它借的。

分发器是干嘛的?分发请求:REQUEST/FORWARD/INCLUDE/ERROR。REQUEST是浏览器发起的,而ERROR是发生页面错误时发生的,稍微特殊些。

所以,所谓Filter更详细的拦截其实是这样:

v2-1d6b0e77752d60f39d829ad39a4a630a_r

灰色块:Filter

最外层那个圈,可以理解成ServletContext,FORWARD/INCLUDE这些都是内部请求。如果在web.xml中配置Filter时4种拦截方式全配上,那么服务器内部的分发跳转都会被过滤。

当然,这些都是可配置的,默认只拦截REQUEST,也就是浏览器来的那一次。

Servlet映射器

上一篇说了很多Servlet的源码,也介绍了Servlet的作用就是处理请求。但是对于每个请求具体由哪个Servlet处理,却只字未提。其实,每一个URL要交给哪个Servlet处理,具体的映射规则都由一个映射器决定:

v2-ff68a99ab3c8a9d9b2bffbc51e22608b_r

这所谓的映射器,其实就是Tomcat中一个叫Mapper的类。

v2-97f7eccfeddccaa932d30d9c23a1af1b_r

它里面有个internalMapWrapper方法:

v2-6de02a1543ac243e79adc217012418ad_r

定义了7种映射规则:

v2-0b486de6085fa9eae71b9203c5560236_r

1.精确匹配 2.前缀匹配

v2-27bf362804b224137c0e51d5d17c8639_r

3.扩展名匹配

v2-e46693fe4049808ba53c4577db61fa12_r

4.5.6 欢迎列表资源匹配

v2-c7ff7422c3a6ac49559e6494b7351590_r

7.如果上面都不匹配,则交给DefaultServlet,就是简单地用IO流读取静态资源并响应给浏览器。如果资源找不到,报404错误

简单来说就是:

对于静态资源,Tomcat最后会交由一个叫做DefaultServlet的类来处理
对于Servlet ,Tomcat最后会交由一个叫做 InvokerServlet的类来处理
对于JSP,Tomcat最后会交由一个叫做JspServlet的类来处理
引用自:tomcat中对静态资源的访问也会用servlet来处理吗?

v2-42c3d43b3b7dd56851d1018d2186d1f0_r

自定义DispatcherServlet

web.xml

v2-98fb7efadcc537b91fb57b00a100285a_r

DispatcherServlet

v2-4102042f7987731d511b905079908581_r

知道了映射器的映射规则后,我们来分析下上图中三种拦截方式会发生什么。

但在此之前,我必须再次强调,我从没说我现在写的是SpringMVC的DispatcherServlet,**这是我自己自定义的一个普通Servlet,**恰好名字叫DispatcherServlet而已。所以,下面的内容,请当做一个普通Servlet的映射分析。

  • *.do:拦截.do结尾

v2-042f720df36b6102a380fde37c5b79c3_r

各个Servlet和谐相处,没问题。

  • /*:拦截所有

v2-b09b1c9c18463d995ef3fd8172725047_r

拦截localhost:8080

v2-c9d5eed6c2a965d61fd36781c1184d95_r

拦截localhost:8080/index.html

v2-5cb839a2c50ffa580ad3621303ec48d5_r

拦截localhost:8080/index.jsp

v2-701e3e3be828b7948bdb74840944dcb4_r

也就是说,/*这种配置,相当于把DefaultServlet、JspServlet以及我们自己写的其他Servlet都“短路”了,它们都失效了。

这会导致两个问题

  • JSP无法被编译成Servlet输出HTML片段(JspServlet短路)

  • HTML/CSS/JS/PNG等资源无法获取(DefaultServlet短路)

  • /:拦截所有,但不包括JSP

v2-e132fe10fc71bd79ce2d1f79964860d4_r

拦截localhost:8080

v2-4df939e9e882ba0fa7c5b74d2fa92329_r

拦截localhost:8080/index.html

v2-d6a36a15180323701c78d0866051dafc_r

不拦截JSP

v2-702efdae9e66ab5de9c71edbf6d0f528_r

虽然JSP不拦截了,但是DefaultServlet还是“短路”了。而DispatcherServlet把本属于DefaultServlet的工作也抢过来,却又不会处理(IO读取静态资源返回)。

怎么办?

DispatcherServlet与SpringMVC

SpringMVC的核心控制器叫DispatcherServlet,映射原理和我们上面山寨版的一样,因为本质还是个Servlet。但SpringMVC提供了一个标签,解决上面/无法读取静态资源的问题:

    <!-- 静态资源处理  css js imgs -->
    <mvc:resources location="/resources/**" mapping="/resources"/>

其他的我也不说了,一张图,大家体会一下DispatcherServlet与SpringMVC到底是什么关系:

v2-ff3412893ecd4b0737ecd2a40447d48f_r

DispatcherServlet确实是一个Servlet,但它只是入口,SpringMVC要比想象的庞大。

conf/web.xml与应用的web.xml

conf/web.xml指的是Tomcat全局配置web.xml。

v2-a6be039280a88dd07d31ec5b87c03e13_720w

它里面配置了两个Servlet:

v2-4262abc58a64cfb800952ceb23355c23_r

v2-d56fa3e8b050cb0cbc78df926528d2f9_r

也就是JspServlet和DefaultServlet的映射路径。

我们可以按Java中“继承”的思维理解conf/web.xml:

conf/web.xml中的配置相当于写在了每一个应用的web.xml中。

相当于每个应用默认都配置了JSPServlet和DefaultServlet处理JSP和静态资源。

如果我们在应用的web.xml中为DispatcherServlet配置/,会和DefaultServlet产生路径冲突,从而覆盖DefaultServlet。此时,所有对静态资源的请求,映射器都会分发给我们自己写的DispatcherServlet处理。遗憾的是,它只写了业务代码,并不能IO读取并返回静态资源。JspServlet的映射路径没有被覆盖,所以动态资源照常响应。

如果我们在应用的web.xml中为DispatcherServlet配置/*,虽然JspServlet和DefaultServlet拦截路径还是.jsp和/,没有被覆盖,但无奈的是在到达它们之前,请求已经被DispatcherServlet抢去,所以最终不仅无法处理JSP,也无法处理静态资源。

Q.E.D.

知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议

弱小和无知不是生存的障碍,傲慢才是。