Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

servlet原理解析

2016-09-17

定义

Servlet 是Server + Applet 的缩写,表示一个服务器应用。

Servlet与Servlet容器的关系类似枪和子弹的关系.枪是为子弹而生,而子弹让枪有了杀伤力.从技术角度来看是为了解耦,他们通过标准化接口来相互协作.

常见servlet容器:

- jetty:在定制化和移动领域
- tomcat

Tomcat的容器等级中,Context容器是直接管理Servlet在容器中的包装类Wrapper,所以Context容器如何运行将直接影响Servlet的工作方式.

image

真正管理Servlet的容器是Context容器,一个Context对应一个web工程,在Tomcat的配置文件中可以很容易发现这一点

<Context path="/projectOne " docBase="D:\projects\projectOne"
reloadable="true" />

Servlet容器的启动过程

Tomcat7也开始支持嵌入式功能,增加了一个启动类 org.apache.catalina.startup.Tomcat.创建一个实例对象并调用start方法就可以很容易启动Tomcat,我们还可以通过这个对象来增加和修改Tomcat的配置参数,如可以动态增加context,Servlet等.下面我们就利用这个Tomcat类来管理增加一个Context容器,我们选择Tomcat7自带的examples Web工程,并看它是如何加到这个Context容器中的.

Tomcat的addWebapp方法代码如下:

public Context addWebapp(Host host, String url, String path) { 
       silence(url); 
       Context ctx = new StandardContext(); 
       ctx.setPath( url ); 
       ctx.setDocBase(path); 
       if (defaultRealm == null) { 
           initSimpleAuth(); 
       } 
       ctx.setRealm(defaultRealm); 
       ctx.addLifecycleListener(new DefaultWebXmlListener()); 
       ContextConfig ctxCfg = new ContextConfig(); 
       ctx.addLifecycleListener(ctxCfg); 
       ctxCfg.setDefaultWebXml("org/apache/catalin/startup/NO_DEFAULT_XML"); 
       if (host == null) { 
           getHost().addChild(ctx); 
       } else { 
           host.addChild(ctx); 
       } 
       return ctx; 
}

一个web应用对应一个Context容器,也就是Servlet运行时的Servlet容器,添加一个web应用时将会创建一个StandardContext容器,并且给这个Context容器设置必要的参数,url和path分别代表这个应用在Tomcat中的访问路径和这个应用实际的物理路径,这两个参数与第一段代码中的地址是一样的.其中最重要的一个配置是ContextConfig,这个类会将负责整个Web应用配置的解析工作,最后将这个Context容器添加到Host中.

接下来会调用Tomcat的start方法启动Tomcat,Tomcat的启动逻辑是基于观察者模式设计的,所有的容器都会继承lifecycle接口,它管理容器的整个生命周期,所有容器的修改和状态的改变都会去通知已经注册的观察者(Listener).

image

上图描述了Tomcat启动过程中,主要类之间的时序关系,下面我们重点关注添加examples应用对应的StandardContext容器的启动过程.

当Context容器初始化未init时,添加Context容器的Listener将会被调用.ContextConfig继承了LifecycleListener接口,它是在调用 Tomcat.addWebapp 时被加入到StandardConext容器中.ContextConfig类会负责整个Web应用的配置文件解析工作.

ContextConfig的init方法将完成以下工作:

  1. 创建用于解析xml文件的contextDigster对象
  2. 读取默认context.xml配置文件,如果存在解析它
  3. 读取默认Host配置文件,如果存在解析它
  4. 读取默认Context自身配置文件,如果存在解析它
  5. 设置Context的DocBase

ContextConfig的init方法完成后,Context容器会会执行startInternal方法,这个方法启动逻辑比较复杂,主要包括如下几个部分:

  1. 创建读取资源文件的对象
  2. 创建ClassLoader对象
  3. 设置应用的工作目录
  4. 启动相关的辅助类如:logger,realm,resource
  5. 修改启动状态,通知感兴趣的观察者(web应用的配置)
  6. 子容器的初始化
  7. 获取ServletContext并设置必要的参数
  8. 初始化”load on startup”的Servlet

web应用的初始化工资

web应用的初始化工作是在ContextConfig的configureStart方法中实现的,应用的初始化主要解析web.xml文件,这个文件描述了一个Web应用的关键信息,也是一个web应用的入口.

Tomcat首先会找globalWebXml这个文件的搜索路径是在engine的工作目录下寻找以下两个文件中的任一个

org/apache/catalin/startup/NO_DEFAULT_XML或者conf/web.xml.

接着会找hostWebXML这个文件可能会在

System.getProperty("catalina.base")/conf/${EngineName}/${HostName}/web.xml.default

接着寻找应用的配置文件examples/WEB-INF/web.xml . web.xml文件中的各个配置项会被解析成相应的属性保存在WebXml .web.xml文件中的各个配置项将会被解析成相应的属性保存在WebXml对象中.如果当前应用支持Servlet3.0,解析还将完成额外9项工作,这个额外的9项工作主要是为Servlet3.0新增的特性,包括jar包中的META-INF/web-fragment.xml的解析以及对annotations的支持.

接下去将会WebXml对象中的属性设置到Context容器中,这里包括创建Servlet对象,filter,listener等.这段代码在WebXml的configureContext方法中.下面是解析Servlet的代码片段:

for (ServletDef servlet : servlets.values()) { 
           Wrapper wrapper = context.createWrapper(); 
           String jspFile = servlet.getJspFile(); 
           if (jspFile != null) { 
               wrapper.setJspFile(jspFile); 
           } 
           if (servlet.getLoadOnStartup() != null) { 
               wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue()); 
           } 
           if (servlet.getEnabled() != null) { 
               wrapper.setEnabled(servlet.getEnabled().booleanValue()); 
           } 
           wrapper.setName(servlet.getServletName()); 
           Map<String,String> params = servlet.getParameterMap(); 
           for (Entry<String, String> entry : params.entrySet()) { 
               wrapper.addInitParameter(entry.getKey(), entry.getValue()); 
           } 
           wrapper.setRunAs(servlet.getRunAs()); 
           Set<SecurityRoleRef> roleRefs = servlet.getSecurityRoleRefs(); 
           for (SecurityRoleRef roleRef : roleRefs) { 
               wrapper.addSecurityReference( 
                       roleRef.getName(), roleRef.getLink()); 
           } 
           wrapper.setServletClass(servlet.getServletClass()); 
           MultipartDef multipartdef = servlet.getMultipartDef(); 
           if (multipartdef != null) { 
               if (multipartdef.getMaxFileSize() != null && 
                       multipartdef.getMaxRequestSize()!= null && 
                       multipartdef.getFileSizeThreshold() != null) { 
                   wrapper.setMultipartConfigElement(new 
MultipartConfigElement( 
                           multipartdef.getLocation(), 
                           Long.parseLong(multipartdef.getMaxFileSize()), 
                           Long.parseLong(multipartdef.getMaxRequestSize()), 
                           Integer.parseInt( 
                                   multipartdef.getFileSizeThreshold()))); 
               } else { 
                   wrapper.setMultipartConfigElement(new 
MultipartConfigElement( 
                           multipartdef.getLocation())); 
               } 
           } 
           if (servlet.getAsyncSupported() != null) { 
               wrapper.setAsyncSupported( 
                       servlet.getAsyncSupported().booleanValue()); 
           } 
           context.addChild(wrapper); 
}

将Servlet包装成Context容器中的StandardWrapper,这里StandardWrapper是Tomcat容器中的一部分,它具有容器的特征,而Servlet为了一个独立的Web开发标准,不应该强耦合在Tomcat中.

除了将Servlet包装成StandardWrapper并作为子容器添加到Context中,其他的所有web.xml属性都被解析到Context中所以说Context容器才是真正运行Servlet的Servlet容器.一个Web应用对应一个Context容器,容器的配置属性由应用的web.xml指定,这样我们就能理解web.xml到底起到什么作用了.

创建Servlet实例

前面已经完成了Servlet的解析工作,并且被包装成StandardWrapper添加在Context容器中,但是它任然不能为我们工作,它还没有被实例化.

创建Servlet对象

如果Servlet的load-on-startup配置项大于0,那么Context容器启动的时候会被实例化,前面提到在解析配置文件时会读取默认的globalWebXml,在conf下的web.xml文件中定义了一些默认的配置项,其定义了两个Servlet,分别是:org.apache.catalina.servlets.DefaultServlet 和 org.apache.jasper.servlet.JspServlet 它们的 load-on-startup 分别是 1 和 3,也就是当 Tomcat 启动时这两个 Servlet 就会被启动。

创建Servlet实例的方法是从Wrapper.loadServlet开始的.loadServlet方法要完成的是获取ServletClass然后把它交给InstanceManager去创建一个基于servleClasss.class的对象.如果这个Servlet配置了jsp-file,那么这个ServletClass就是conf.web.xml中定义的org.apache.jasper.servlet.JspServlet了.

创建Servlet对象相关类结构如下:

image

初始化Servlet

初始化Servlet在StandardWrapper的initServlet方法中,这个方法很简单就是调用Servlet的init的方法,同时把保包装了StandardWrapper对象的StandardwrapperFacade作为ServletConfig传给Servlet.

如果该Servlet关联的是一个jsp文件,那么前面初始化的就是JspServlet,接下去会模拟一次简单请求,请求调用这个jsp文件,以便编译这个jsp文件为class,并初始化这个class.

这样Servlet对象就初始化完成了,事实上Servlet从被web.xml中解析到完成初始化,这个过程非常复杂,包括各种容器状态的转化引起的监听事件的触发,各种访问权限的控制和一些不可预料错误发生的判断行为等待.

Servlet初始化时序图:

image

Servlet体系结构

我们知道java Web应用是基于Servlet规范运转的,那么Servlet本身又是如何运转的呢?

Servlet顶层类关联图

image

分析:

与Servlet主动关联的三个类,分别是ServletConfig,ServletRequest和ServletResponse.

这三个类都是通过容器传递个Servlet的,其中ServletConfig是在初始化时就传递给Servlet了,而后两个是在请求到达时调用Servlet时传递过来的.

ServletConfig和ServletContext的作用:

  • ServletConfig: ServletConfig接口中的方法都是为了获取这个Servlet的一些配置属性,这些属性可能在Servlet运行时用到.

  • ServletConttext:Servlet的运行模式是一个典型的”握手型的交互模型”,两个模块为了交换数据都会准备一个交易场景,这个场景一直跟随这个交易过程知道这个交易完成为止.这个交易场景的初始化时根据这次交易对象指定的参数来定制的,这些指定参数通常就是一个配置类.

所以,交易场景就由ServletContext来描述,而定制的参数集合就由ServletConfig.而ServleRequest和ServletResponse就是要交互的具体对象了,它们通常都是作为运算工具来传递交互结果.

ServletConfig 是在 Servlet init 时由容器传过来的,那么 ServletConfig 到底是个什么对象呢?

ServletConfig和ServletContext在Tomcat容器中的类关系图.

image

StandardWrapper和StandardWrapperFacade都实现了ServletConfig接口,而StandardWrapperFacade是StandardWrapper门面类.所以传给Servlet的是StandardWrapperFacade对象,这个类能够保证从StandardWrapper中拿到ServletConfig所规定的数据,而又不把ServletConfig不关心的数据暴露给Servlet.

同样ServletContext也与ServletConfig有类似的结构,Servlet中能拿到的ServletContext的实际对象也是ApplicationContextFacade对象.ApplicationContextFacade同样保证ServletContext只能从容器中拿到它该拿的数据,他们都起到对数据的封装作用,他们使用的都是门面设计模式.

通过ServletContext可以拿到Context容器中的一些必要信息,比如应用的工作路径,容器支持的Servlet最小版本.

** Servlet中定义的两个ServletRequest和ServletResponse它们实际对象又是什么呢?**

我们再创建自己的Servlet类时通常使用的都是HttpServletRequest和HttpServletResponse,它们继承了ServletRequest和ServletReponse.

Request相关类的结构图

image

Tomcat一接受请求首先会创建org.apache.coyote.Request 和 org.apache.coyote.Response.这两个类时Tomcat内部使用的描述一次请求和响应的信息类它们是一个轻量级的类,它们作用就是在服务器接收到请求后,经过简单解析将这个请求快速的分配给后续线程去处理,所以它们的对象很小,很容易被JVM回收.接下去当交给一个用户线程去处理这个请求时又创建org.apache.catalina.connector. Request 和 org.apache.catalina.connector. Response 对象。这两个对象一直穿越整个Servlet容器直到要传给Servlet,传给Servlet的是Request和Response的门面类RequestFacade和RequestFacade,这里使用门面模式与前面一样都是基于同样的目的–封装容器中的数据.一次请求对应的Request和Response的类转化如下图:

image

Servlet如何工作

当用户从浏览器向服务器发起一个请求,通常会包含如下信息:http://hostname:port/contextpath/serverletpath,hostname和port是用来与服务器建立TCP连接,而后面的Url才是用来选择服务器中那个自容器服务用户请求.

服务器如何根据URL来达到正确的Servlet容器中的目的呢?

Tomcat7.0中这件事很容易解决,因为这种映射工作有专门一个类来完成的,这个就是org.apache.tomcat.util.http.mapper,这个类保存了Tomcat的Containse容器中的所有子容器的信息,当org.apache.catalina.connector. Request 类在进入Container容器之前,mapper将会根据这次请求的hostname和contextpath将host和context容器设置到Request的MappingData属性中.所以当Request进入Container容器之前,它要访问那个子容器这时已经确定了.

Request 的 Mapper 类关系图

image

public void init() { 
       findDefaultHost(); 
       Engine engine = (Engine) connector.getService().getContainer(); 
       engine.addContainerListener(this); 
       Container[] conHosts = engine.findChildren(); 
       for (Container conHost : conHosts) { 
           Host host = (Host) conHost; 
           if (!LifecycleState.NEW.equals(host.getState())) { 
               host.addLifecycleListener(this); 
               registerHost(host); 
           } 
       } 
}

这段代码的作用就是将 MapperListener 类作为一个监听者加到整个 Container 容器中的每个子容器中,这样只要任何一个容器发生变化,MapperListener 都将会被通知,相应的保存容器关系的 MapperListener 的 mapper 属性也会修改。for 循环中就是将 host 及下面的子容器注册到 mapper 中。

Request在容器中的路由图

image

上图描述了一次Request请求是如何到达最终的Wrapper容器的,但是请求到达最终的Servlet还要完成一些步骤,必须要执行Filter链,以及要通知你再web.xml中定义的listener.

接下去就是执行Servlet的service方法了,通常情况下,我们自己定义的Servlet并不是直接去实现javax.servlet.servlet接口,而是去继承更简单的HttpServlet类或者GenericServlet类,我们可以有选择的覆盖相应方法去实现我们要完成的工作.

Servlet的确已经能够帮助我们完成所有工作了,但现在web应用很少有直接将交互全部页面都用Servelt实现,而是采用更加高效的MVC框架来实现.这些MVC框架基本的原理都是将所有请求都映射到一个Servlet,然后去实现Service方法,这个方法也就是MVC框架的入口.

当Servlet从Servlet容器中移除时,也就标明该Servlet的生命周期结束了,这时Servlet的destroy方法将被调用,做一些扫尾工作.

Servlet能够给我们提供两部分数据:

  • ServletConfigservlet: ServletConfigservlet初始化时调用init方法时设置的ServletConfig,这个类基本上含有了Servlet本身和Servlet所运行的Servlet容器中的基本信息.servletConfig的实际对象时StandardWrapperFacade,到底能获得哪些容器信息可以看提供了哪些接口.
  • ServletRequest类提供,它的世界对象是RequestFacade,主要描述这次请求的Http协议信息.

Session如何基于Cookie来工作:

  • 基于URL Path Parameter,默认就支持
  • 基于Cookie,如果你没有修改Context容器的cookies标识的话,默认也是支持的
  • 基于SSL,默认不支持,只有connector.getAttribute(“SSLEnabled”)为true时才支持

第一种情况下:当浏览器不支持cookie功能时,浏览器将用户的SessionCookieName重写到用户请求的URL参数中,它的传递格式如 /path/Servlet?name=value&name2=value2&Name3=value3,其中”Servlet”后面的K-V对就是要传递的Path Parameters,服务器会从这个Path Parameters中拿到用户配置的SessioionCookieName.关于这个SessionCookieName ,如果你在web.xml中胚子sessionConfig配置项的话,其cookile-config下的name属性就是这个sessionCookieName值,如果没有配置session-config配置项,默认的SessionCookieName就是大家熟悉的JSESSIONID.接着Reques根据这个SessionCookieName到parameters拿到Sesiion ID并设置到reques.setRequestSessionId中.

请注意如果客户端也支持 Cookie 的话,Tomcat 仍然会解析 Cookie 中的 Session ID,并会覆盖 URL 中的 Session ID。

如果是第三种情况的话会根据javax.servlet.request.ssl_session属性设置Session ID

有了Session ID 服务器就可以创建httpSession对象了,第一次触发是通过request.getSession()方法,如果当前的Session ID 还没有对应的HttpSession对象那么就创建一个新的,并将这个对象加到org.apache.catalina. Manager 的 sessions 容器中保存,Manager类将管理所有Session的生命周期,Session过期将被回收,服务器关闭,session将被序列化到磁盘等.只要这个HttpSession存在,用户就可以根据Session ID来获取这个对象,也就达到了状态的保持.

image

上从图中可以看出从 request.getSession 中获取的 HttpSession 对象实际上是 StandardSession 对象的门面对象,这与前面的 Request 和 Servlet 是一样的原理。下图是 Session 工作的时序图:

image

Servlet 中的 Listener

整个Tomcat服务器中Listener使用非常广泛,它是基于观察者模式设计的,Listener的设计开发Servlet应用程序提供了一种快捷的手段,能够方便的从另一个纵向维度控制程序和数据.目前Servlet中提供了5种两类事件的观察者接口,它们分别是:

  • ServletContextAttributeListener
  • ServletRequestAttributeListener
  • ServletRequestListener
  • HttpSessionAttributeListener

以及ServletContextListener,HttpSessionListener 如下图所示:

image

它们基本上涵盖了整个 Servlet 生命周期中,你感兴趣的每种事件。这些 Listener 的实现类可以配置在 web.xml 中的 标签中。当然也可以在应用程序中动态添加 Listener,需要注意的是 ServletContextListener 在容器启动之后就不能再添加新的,因为它所监听的事件已经不会再出现。掌握这些 Listener 的使用,能够让我们的程序设计的更加灵活。


转载自: developerworks


上一篇 自己实现HTTP

评论