Servlet的历史与规范

一、Servlet历史

1. Servlet的由来

  • 背景
    上世纪90年代,随着Internet和浏览器的飞速发展,基于浏览器的B/S模式随之火爆发展起来。
    最初,用户使用浏览器向WEB服务器发送的请求都是请求静态的资源,比如html、css等。
    但是可以想象:根据用户请求的不同动态的处理并返回资源是理所当然必须的要求。

  • CGI
    必须要满足上述需求,所以CGI(Common Gateway Interface)出现了。CGI程序使用C、Shell Script或Perl编写,CGI是为特定操作系统编写的(如UNIX或Windows),不可移植,CGI程序对每个请求产生新的进程去处理。步骤如下:

    1. WEB服务器接收一个用户请求;
    2. WEB服务器将请求转交给CGI程序处理;
    3. CGI程序将处理结果返回给WEB服务器;
    4. WEB服务器把结果送回用户;
      这里写图片描述
  • Java
    与此同时,Java语言也在迅速发展。必然的,Java要支持上述需求。
    Java有两种方案来实现动态需求,它们都属于JavaEE技术的一部分。

    1. applet
      这是纯客户端(浏览器)方案,applet就是浏览器中的Java插件,浏览器通过它就能够解释执行WEB服务器发过来的Java代码,从而实现动态。但是,显然这种方案不好,既需要浏览器必须安装插件,又受限于浏览器,所以Java代码不能太多和太复杂。

      比如,如果安装了JRE,虽然IE浏览器会自动启用Java插件,但是你可以轻易禁止。再比如Chrome还需要你手动去安装插件才行,普通用户连Java是什么都不知道他怎么会去装呢?
      IE如下图:

      这里写图片描述

    2. Servlet
      既然浏览器不方便执行Java代码,那自然还是服务端来执行了,所以Servlet出现了,Servlet就是server端的applet的意思。

2. Servlet的工作原理

其实Servlet的工作原理基本类似上面的CGI,不过Servlet比CGI更好。

  1. WEB服务器接收一个用户请求;

  2. WEB服务器将请求转交给WEB服务器关联的Servlet容器;

  3. Servlet容器找到对应的Servlet并执行这个Servlet;

  4. Servlet容器将处理结果返回给WEB服务器;

  5. WEB服务器把结果送回用户;

3. Servlet的发展

  1. Servlet诞生后,SUN公司很快发现了Servlet编程非常繁琐,这是因为:

    • Servlet代码中有大量冗余代码,每个Servlet都有一模一样的或基本近似的代码,比如out输出你可能就得写成百遍;
    • 开发Servlet必须精通网页前端和美工,你得非常不直观的在Servlet中写前端代码,这使得实现各种页面效果和风格非常困难。
  2. 所以,SUN借鉴了Microsoft的ASP,正式提出JSP(Servlet1.1),已期望能代替Servlet。但是很快,SUN发现JSP也有问题:

    • 前端开发人员需要看JSP中大量的令他困惑的后端代码;
    • 同样,Servlet开发人员也得在复杂的前端代码中找到其能写Servlet代码的地方;
  3. 所以,Servlet1.2出现了,这个版本的Servlet倡导了MVC思想:

    • JSP(V):将后端代码封装在标签中,使用大量的标签,JSP只用来写前端代码而不要有后台代码;
    • Servlet(C):Servlet完成Controller的功能再加上部分代码逻辑;
    • Model(M):Servlet将数据发送给Model,Model包括部分代码逻辑,最主要的Model也代表着被组织好的用于返回的数据。最终,Model数据会被显示在JSP上(V)。

基本上到这里Servlet的大方向已经固定了,随之,成熟的发展至今 - 2016年5月26日…


↑以上,是关于Servlet的历史部分。↓下面来讲一讲Servlet规范中重要知识点。


声明:以下内容归纳自官方Servlet规范和JavaEE规范等文档。

二、Servlet规范

下载地址
Servlet规范官方地址:JSR 340: Java Servlet 3.1 Specification(中文版网上有人翻译了,可以自己搜索找找)
可以自己下载阅读,最终版final是2013年5月28发布的Servlet3.1。

1. Servlet概述

Servlet有两种意思:

  1. 广义上是:基于Java技术的Web组件,被容器托管,用于生成动态内容。

    再详细点说,Servlet是JavaEE组件中的 -> Web组件的 -> 一种。
    (其它两种是JavaServer Faces和JavaServer Page)

  2. 狭义上说:是JavaEE API中的一个interfacejavax.servlet.Servlet

Servlet 容器/引擎:

  1. Servlet容器也可以叫引擎,Container/Engine,用于执行Servlet

  2. 容器是以内嵌或者附加组件的形式存在于Web服务器或者应用服务器中的。

  3. 容器本身(不依赖Web服务器)就提供了基于请求/响应发送模型的网络服务,解码基于MIME的请求,格式化基于MIME的响应。

  4. 所有容器必须实现HTTP协议的请求/响应模型。其它协议不强求,如HTTPS。




下面开始说一下规范的核心要点。
请注意:我不是要完整的阐述Servlet规范,毕竟你可以直接看规范。这里我只是要记录我认为重要的点。

为了方便描述,先声明一些名词:

  • web.xml = 部署描述符(Deployment Descriptor )
  • 容器 = Servlet Container/Engine



2. Servlet Interface

Servlet生命周期:

Servlet的生命(周期)是由容器管理的,换句话说,Servlet程序员不能用代码控制其生命。

  1. 加载和实例化:
    时机取决于web.xml的定义,如果有<load-on-startup>x</load-on-startup>则在容器启动时,反之则在第一次针对这个Servlet的请求发生时。

  2. 初始化:
    实例化后会立马进行初始化。也就是执行init方法。

  3. 请求处理:
    初始化后,Servlet就可以接受请求了。

    • 基本方式是执行Servlet接口中的service方法。

    • 当然,API也提供了HttpServlet抽象类,其中有doGetdoPost等特殊方法。

    • 注意:任意的容器按照规范必须实现上述几种方法,所以你的代码写在这几个方法中都可以。

  4. 终止服务:

    1. 容器会在合适的时候销毁某个Servlet对象,这个策略取决于容器的开发者/商。

    2. 在容器关闭的时候Servlet对象一定会被销毁。

    3. 当1或2发生时,也就是Servlet对象被销毁时,destroy方法会被调用。

3. Request

1. 请求路径元素

  1. Context Path

    • 通常以'/'开头,但不以'/'结尾;
    • 如果是容器默认Context,则为空字符串:""
  2. Servlet Path

    • 首先,这与自己配置的<url-pattern>有关(具体理解可看下图);
    • 其次,通常以'/'开头;
    • 最后,如果是匹配"/*"""的话则为空字符串"";
  3. PathInfo

    • 取决于Servlet Path划走了多少;
    • 通常要么为null要么以'/'开头;

    例如:下图首先展示了1个Context Path和其下3个Servlet的配置,然后给出了3个Request Path的例子来具体分析划分情况

    这里写图片描述
    这里写图片描述

2. 请求编码

  1. 请求会以什么编码形式送给服务器端呢?

    HTTP协议没有强制规定,所以实际上这是由浏览器自己决定的,决定后浏览器可以通过entity-body中的Content-Type项告诉服务器自己使用了什么编码。但是!大部分情况下浏览器不会这么做的。比如说,Get请求是没有entity-body的,自然也不会使用Content-Type了。

  2. 在服务端,我们Servlet规范 规定了如果请求没有指定编码的话,容器必须使用IOS-8859-1来解码。为了让开发人员知道请求给没给出编码,容器会在没给的情况下通过getCharacterEncoding 返回null来告诉我们。

  3. 为了在我们明知道不是ISO-8859-1编码的情况下给我们自主权,ServletRequest提供了setCharacterEncoding(String enc);

4. Servlet Context

一个Web应用对应一个ServletContext接口的实例。

1. 获取资源:
ServletContext接口提供了直接访问Web应用中静态内容(意思是说你获取jsp返回就是jsp源码)层次结构的文件的方法。

  • getResource

  • getResourceAsStream

    这两个方法需要的String参数必须是以'/'开头的,这个'/'代表相对于:

    • ServletContextPath的路径。

    • 或者WEB-INF/lib中的jar中的METE-INF/resources路径。

5. Response

代表容器的响应,没有特别需要注意的。

6. Filtering

过滤器,是Java中一种代码重用技术,通过拦截请求改变HTTP请求的内容、响应、Header信息。

  1. 实例化、初始化:

    容器启动时,自上而下的实例化并初始化Filter

  2. 责任链

    匹配的Filter很可能是多个而不是一个,所以Filter是一个FilterChain设计。

    所有FilterdoFilter(request, response, chain);方法最终都需要调用chain.doFilter(request, response);方法来触发调用链的下一个Filter或者如果是最后一个Filter那么直接访问目标资源。

7. 映射请求到Servlet的规则

  1. 用于映射到Servlet的路径是:

    用于映射到Servlet的路径 = URL - (ServletContext + 路径参数)

    例如当客户端请求的URL是:http://www.google.com/testproject/action/servlet1?param1=asd时,那么映射到的Servlet路径是:/action/servlet1

  2. 重要!重要!重要!选择映射到的Servlet的规则是,按照如下的顺序查找,如果已经选定一个就会匹配成功不会继续往下:

    1. 先精确匹配<url-pattern>,成功则选择。(精确匹配

    2. 递归遍历路径树,选择最长的路径匹配。(最长匹配

    3. 如果URL最后一个部分包括扩展名,比如/action/servlet1.jsp,容器将选择专门声明了要处理此扩展名请求的Servlet。(扩展名匹配

    4. 如果123都没有匹配,容器将提供一个后备方案,一般来说是提供一个"default" Servlet。(低保匹配,优先级最低,提供一个最低保障)

  3. 映射规范

    1. '/'字符开始,以'/*'字符结束的字符串:用于路径匹配。(这是一个准确严谨的定义而已)

    2. '*.'开始的字符串用于扩展名映射

    3. 空字符串""是特殊的URL,精确映射到应用的上下文根,即http://host:port/<context-root>/,这种情况("")相当于<url-pattern>/<url-pattern>

    4. 只包含'/'字符的字符串表示应用的"default" Servlet

      • 此时,Servlet Path = 请求URL - Context Path,且Path Info = null

      • Spring MVC 配置front Controller(DispatcherServlet)时,<url-pattern>/<url-pattern>就可以这样做;

      • 需要注意:这个"/"不代表匹配所有,只是代表当没有其它Servlet匹配这个请求时,"default" Servlet去擦屁股。

  4. 隐式映射

    容器可以为一些扩展名定义一些隐射映射,比如来一个.jsp的,那么如果上面的显示映射没有拦截.jsp,此时这里应该发挥作用。

  5. tomcat中是怎么做的(我看了Jetty也是差不多)

    1. tomcat在其顶级的web.xml中定义了且开放了2个<servlet>(这么说是因为其实不只2个,不过那几个是注释状态)

      <servlet>
          <servlet-name>default</servlet-name>
          <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
          ...
          <load-on-startup>1</load-on-startup>
      </servlet>
      
      <servlet>
          <servlet-name>jsp</servlet-name>
          <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
          ...
          <load-on-startup>3</load-on-startup>
      </servlet>
      
      <!-- default servlet mapping -->
      <servlet-mapping>
          <servlet-name>default</servlet-name>
          <url-pattern>/</url-pattern>
      </servlet-mapping>
      
      <!-- The mappings for the JSP servlet -->
      <servlet-mapping>
          <servlet-name>jsp</servlet-name>
          <url-pattern>*.jsp</url-pattern>
          <url-pattern>*.jspx</url-pattern>
      </servlet-mapping>

      可以看到tomcat践行了如上所述的Servlet规范。

    2. jsp:org.apache.jasper.servlet.JspServlet这个Servlet最终会:

      • 先将.jsp文件变为java文件(即一个Servlet);
      • 然后再compile.class
    3. default:org.apache.catalina.servlets.DefaultServlet这个Servlet最终会:

      • 为所有没有被匹配到的URL做匹配,即”低保”匹配;
      • 如果找到相应的资源了就返回,没有找到或者发生一些异常就返回400/404等状态码及其页面,长相如下:
        这里写图片描述

8. Session

  1. 会话跟踪机制

    HTTP协议是无状态的,但是记录状态,也就是说记录来自同一客户端的请求的需求是必须的。所以人们发明了会话跟踪机制。

    Servlet规范定义了一个简单的HttpSession接口,允许容器使用几种方法来实现会话跟踪,从而使得Web应用开发人员不必来关心和写这块的代码。(容器只能帮我们实现单机会话跟踪,分布式应用多机状态下,我们需要自己写代码实现Session同步)。

  2. 几种方法

    1. Cookies
      是最常用的机制,且所有Servlet容器必须支持。容器有能力(但是需要程序员显式调用)向客户端发送一个cookie,用来记录会话,标准名字必须是JSESSIONID

    2. URL重写
      比如:http://www.sss.com/aa/bb.html;jessionid=1234
      要注意,使用的是分号';',这个叫路径参数,区别于查询参数。

  3. 如何使用Session跟踪机制

    1. 需要程序员手动调用

      //a. 有session返回,没有新生成一个
      request.getSession();
      request.getSession(true);
      //b. 有session返回,没有返回null
      request.getSession(false);
    2. 如果使用的a方式,再加上使用cookie机制的情况下,容器的第1次响应(Response)会向客户端写一个JSESSIONID,客户端从第2次请求(Request)开始会带着JSESSIONID,如下图:

      这里写图片描述

      这里写图片描述

9. Web Application

1. WEB-INF目录:
此目录是一个特殊目录,不能由容器直接提供给客户端访问。可以通过:

  1. 调用ServletContextgetResourcegetResourceAsStream来访问。

  2. 还可以通过RequestDispatcher来调用从而公开这些内容。

2. WEB-INF目录的内容:

  1. /WEB-INF/web.xml部署描述文件。

  2. Servlet和其它类的目录/WEB-INF/classes/

  3. Java归档文件(jar)区域/WEB-INF/lib/*.jar

10. Application Lifecycle Events - 应用生命周期事件

Servlet API为ServletContextHttpSessionServletRequest这三个对象添加了事件。这可以让Servlet开发人员更好的控制上述3个对象生命周期。

  1. ServletContext

    事件类型 描述 监听器接口
    生命周期 ServletContext刚创建并可用于服务它的第一个请求或即将关闭 javax.servlet.ServletContextListener
    更改属性 ServletContext的属性已添加、已删除、已替换 javax.servlet.ServletContextAttributeListener
  2. HttpSession

    事件类型 描述 监听器接口
    生命周期 会话已创建、销毁、超时 javax.servlet.http.HttpSessionListener
    更改属性 HttpSession的属性已添加、已删除、已替换 javax.servlet.http.HttpSessionAttributeListener
    改变ID HttpSession的ID将被改变 javax.servlet.http.HttpSessionIdListener
    会话迁移 HttpSession已被激活或钝化 javax.servlet.http.HttpSessionActivationListener
    对象绑定 对象已经从HttpSession绑定或解绑 javax.servlet.http.HttpSeesionBindingListener
  3. ServletRequest

    事件类型 描述 监听器接口
    生命周期 一个请求已经开始由Web组件处理 javax.servlet.ServletRequestListener
    更改属性 已在servlet上添加、移除、替换属性 javax.servlet.ServletRequestAttributeListner
    异步事件 超时、连接终止或完成异步操作处理 javax.servlet.AsyncListener

实例化时机:

容器必须在开始执行进入应用的第一个请求之前完成Web应用中所有监听器类的实例化。

11. Deployment Descriptor - web.xml

1. 关于顺序

  1. Servlet初始化顺序

    通过<load-on-startup>x</load-on-startup>中的x来指定初始化顺序,必须不小于0,越小越早加载。

  2. Filter

    1. 初始化顺序:

      按照web.xml中声明的顺序自上而下的初始化。

    2. 过滤器链构造规则:

      规范中6.2.4 节,待验证总结。

    3. 过滤顺序

      如果匹配了多个url-pattern,按照自上而下的顺序。

  3. Listener调用顺序

    根据在web.xml中注册的顺序来被调用。而销毁事件触发的destroy会被反方向的依次调用。

2. 关于初始化参数:

  1. ServletContext的初始化参数:

    1. 设值:因为一个应用只有一个ServletContext,所以是直接声明在根<web-app>下的:

      <context-param>
         <param-name>contextParam1</param-name>
         <param-value>11context11</param-value>
      </context-param>
    2. 取值/设值:只要获取到了ServletContext对象,就可以使用其getInitParameter(String name)方法来取值,同时也可以使用setInitParameter(String name, String value);来设值,所以很多地方都可以取值/设值。

  2. Listener的初始化参数:

    1. 设值:监听器没有自己独立的初始化参数配置,想要使用的话可以借助将参数配置在ServletContext的初始化参数位置。

    2. 取值/设值:如上。

  3. Filter的初始化参数:

    1. 设值:只能在<filter>中使用<init-param>来设置值:

      <filter>
          <filter-name>FirstFilter</filter-name>
          <filter-class>filter.FirstFilter</filter-class>
          <init-param>
              <param-name>firstFilterParam1</param-name>
              <param-value>11filter11</param-value>
          </init-param>
      </filter>
    2. 取值:只有获取了某一个FilterFilterConfig对象之后,才能使用此对象的方法getInitParameter(String name)方法来取值。

    3. 另外的:当你自己写一个Filter的时候,除了实现Filter接口之外,你还可以选择学习类似GenericServlet的方式,额外的实现FilterConfig接口并覆盖其getInitParameter(String name)方法,从而可以在自己的Filter的任何方法中都能调用取值方法而无须显示获取FilterConfig对象。像下面这样:

      public class FirstFilter implements Filter, FilterConfig{
      ...
      }
  4. Servlet的初始化参数:

    1. 设值:只能在<servlet>中使用<init-param>来设置值:

      <servlet>
          <servlet-name>FirstServlet</servlet-name>
          <servlet-class>servlet.FirstServlet</servlet-class>
          <init-param>
              <param-name>FirstServletParam1</param-name>
              <param-value>11FirstServlet11</param-value>
          </init-param>
      </servlet>
    2. 取值:跟Filter类似,ServletServletConfig对象,只要获取到它就可以使用其getInitParameter(String name)方法来取值。

    3. 另外的:通常我们不会直接实现Servlet接口而是使用继承GenericServlet/HttpServlet的方式,那么(如我在上面Filter“另外的”部分所说)我们就可以利用它们的实现直接使用getInitParameter(String name)方法来取值。


转载注明出处:http://blog.csdn.net/u010297957/article/details/51498018

阅读更多
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u010297957/article/details/51498018
文章标签: servlet
个人分类: JavaEE
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭