天天看点

从servlet容器说起1 Servlet容器的启动过程2 Web 应用的初始化工作

要介绍 Servlet 必须要先把 Servlet 容器说清楚,Servlet 与 Servlet 容器的关系有点像枪和子弹的关系,枪为弹而生,而弹又让枪有了杀伤力。虽然它们是彼此依存的,但是又相互独立发展,这一切都是为了适应工业化生产。从技术角度来说是为了解耦,通过标准化接口来相互协作。既然接口是连接 Servlet 与 Servlet 容器的关键,那我们就从它们的接口说起。

Servlet 容器作为一个独立发展的标准化产品,目前种类很多,但是它们都有自己的市场定位,很难说谁优谁劣。以大家最为熟悉 Tomcat 为例来介绍 Servlet 容器如何管理 Servlet。Tomcat 本身也很复杂,我们只从 Servlet 与 Servlet 容器的接口部分开始介绍,关于 Tomcat 的详细介绍可以参考我的网上其他博文

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

从servlet容器说起1 Servlet容器的启动过程2 Web 应用的初始化工作

Tomcat容器模型

从上图可以看出 Tomcat 的容器分为四个等级,真正管理 Servlet 的容器是 Context 容器,一个 Context 对应一个 Web 工程,在 Tomcat 的配置文件中可以很容易发现这一点,如下:

从servlet容器说起1 Servlet容器的启动过程2 Web 应用的初始化工作

Context.xml配置文件内容

下面详细介绍 Tomcat 解析 Context 容器的过程,包括如何构建 Servlet

1 Servlet容器的启动过程

Tomcat7 开始支持嵌入式功能,增加了一个启动类 org.apache.catalina.startup.Tomcat

创建一个实例对象并调用 start 方法就可以很容易启动 Tomcat,我们还可以通过这个对象来增加和修改 Tomcat 的配置参数,如可以动态增加 Context、Servlet 等.

下面我们就利用这个 Tomcat 类来管理新增的一个 Context 容器,我们就选择 Tomcat7 自带的 examples Web 工程,并看看它是如何加到这个 Context 容器中的.

Tomcat tomcat = getTomcatInstance(); 
 File appDir = new File(getBuildDirectory(), "webapps/examples"); 
 tomcat.addWebapp(null, "/examples", appDir.getAbsolutePath()); 
 tomcat.start(); 
 ByteChunk res = getUrl("http://localhost:" + getPort() + 
               "/examples/servlets/servlet/HelloWorldExample"); 
 assertTrue(res.toString().indexOf("<h1>Hello World!</h1>") > 0);
           

上段创建一个 Tomcat 实例并新增一个 Web 应用,然后启动 Tomcat 并调用其中的一个 HelloWorldExample Servlet,看有没有正确返回预期的数据。

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 中的访问路径和这个应用实际的物理路径,这个两个参数与清单 1 中的两个参数是一致的。
  • 其中最重要的一个配置是 ContextConfig,这个类将会负责整个 Web 应用配置的解析工作,后面将会详细介绍。
  • 最后将这个 Context 容器加到父容器 Host 中。

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

从servlet容器说起1 Servlet容器的启动过程2 Web 应用的初始化工作

Tomcat 主要类的启动时序图

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

当 Context 容器初始化状态设为 init 时,添加在 Contex 容器的 Listener 将会被调用。

ContextConfig 继承了 LifecycleListener 接口,它是在调用Tomcat 的 addWebapp 方法时被加入到 StandardContext 容器中的。

ContextConfig 类会负责整个 Web 应用的配置文件的解析工作。

ContextConfig 的 init 方法将会主要完成以下工作

  • 创建用于解析 xml 配置文件的 contextDigester 对象
  • 读取默认 context.xml 配置文件,如果存在则解析它
  • 读取默认的 Host 配置文件,如果存在则解析它
  • 读取默认的 Context 自身的配置文件,如果存在则解析它
  • 设置 Context 的 DocBase

ContextConfig 的 init 方法完成后,Context 容器就会执行 startInternal 方法

  • 创建读取资源文件的对象
  • 创建 ClassLoader 对象
  • 设置应用的工作目录
  • 启动相关的辅助类如:logger、resources
  • 修改启动状态,通知感兴趣的观察者(Web 应用的配置)
  • 子容器的初始化
  • 获取 ServletContext 并设置必要的参数
  • 初始化“load on startup”的 Servlet

2 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 对象中。

如果当前应用支持 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,为什么要将Servlet包装成StandardWrapper 而不直接是 Servlet 对象呢?StandardWrapper 是 Tomcat 容器的一部分,它具有容器的特征,而 Servlet 作为一个独立的 web 开发标准,不应该强耦合在 Tomcat 中

除了将 Servlet 包装成 StandardWrapper 并作为子容器添加到 Context 中,其它的所有 web.xml 属性都被解析到 Context 中,所以说 Context 容器才是真正运行 Servlet 的 Servlet 容器。

一个 Web 应用对应一个 Context 容器,容器的配置属性由应用的 web.xml 指定,这样我们就能理解 web.xml 到底起到什么作用了。

继续阅读