[web]Servlet工作原理

释放双眼,带上耳机,听听看~!

  概述:互联网Web技术时当今主流,而Servlet是Java Web技术的核心基础,掌握Servlet工作原理是每一个Java Web开发技术人员的基本功。框架技术千变万化,Java 核心不离其宗。花点时间耐心看完,我们一起学习Java Web技术时如何基于Servlet工作的?包括Web应用如何在Servlet容器中如何启动?Servlet容器如何解析我们的项目配置web.xml.?用户请求如何到达指定的Servlet?Servlet容器如何管理Servlet生命周期?

一、从Servlet容器说起

  要了解Servlet,还需要从Servlet容器开始说起,以Tomcat为例,讲解Servlet容器是如何管理我们的Servlet的?

  在Tomcat的容器等级中,Context容器才是真正管理Servlet的在容器中的包装类Wrapper,所以Context容器的运行方式直接影响Servlet的工作方式。一个Context容器对应了Tomcat中的一个项目。

[web]Servlet工作原理

 

二、Servlet容器的启动过程

  一般我们都是把开发好的web项目放在Tomcat指定的目录下,然后启动Tomcat,那么我们在添加一个项目的这个动作,其实Tomcat内部是调用了一个addWebapp的方法,该方法的源码如下,看看都做了哪些工作?(相关的我在代码中添加了注释)

 1     public Context addWebapp(Host host, String contextPath, String docBase,
 2             LifecycleListener config) {
 3 
 4         silence(host, contextPath);
 5 
 6         // 创建一个 StandardContex t容器,并设置相关的参数,path,项目资源路径等
 7         Context ctx = createContext(host, contextPath);
 8         ctx.setPath(contextPath);
 9         ctx.setDocBase(docBase);
10 
11         // 是否添加默认的web配置到该项目中
12         if (addDefaultWebXmlToWebapp)
13             ctx.addLifecycleListener(getDefaultWebXmlListener());
14 
15         // 设置StandardContex容器的配置文件
16         ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
17 
18         // 添加一个监听器到容器中
19         ctx.addLifecycleListener(config);
20 
21         if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) {
22             // prevent it from looking ( if it finds one - it\'ll have dup error )
23             // 将传入的参数 LifecycleListener 转为 ContextConfig
24             // ContextConfig 将负责整个web应用的配置解析工作
25             ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
26         }
27 
28         if (host == null) {
29             getHost().addChild(ctx);
30         } else {
31             host.addChild(ctx);
32         }
33 
34         return ctx;
35     }

  将项目添加完成后,就可以调用Tomcat的start方法启动了。Tomcat的启动逻辑是基于观察者模式设计的,所有的容器都继承了Lifecycle接口,该接口管理者整个容器的生命周期,所有容器的修改和状态的改变都将由它去通知已经注册的观察者。该接口中定义的方法如下图:

[web]Servlet工作原理

  细心的话我们就会发现,刚才在 addWebapp的方法中,有一个参数就是  LifecycleListener ,它就是容器注册的观察者。在这里就不仔细深入Tomcat的启动过程了,我们主要关注一下 每一个Web 应用都会创建的 StandardContex 容器,它的启动过程到底是怎么样的?

  在上面addWebapp的源码中,第7行的位置创建了一个 StandardContex 容器,当Context容器的状态为init时,上面源码19行添加到Context容器的 LifecycleListener  将被调用,在第25行的时候被强转为 ContextConfig, ContextConfig 实现了LifecycleListener  接口,负责整个web应用的配置文件解析工作。

ContextConfig 首先调用init 方法:

 1     /**
 2      * Process a \"init\" event for this Context.
 3      */
 4     protected synchronized void init() {
 5         // Called from StandardContext.init()
 6 
 7         // 创建解析XML的 contextDigester
 8         Digester contextDigester = createContextDigester();
 9         contextDigester.getParser();
10 
11         if (log.isDebugEnabled()) {
12             log.debug(sm.getString(\"contextConfig.init\"));
13         }
14         context.setConfigured(false);
15         ok = true;
16 
17         // 利用创建的解析器 contextDigester 解析默认的配置文件
18         contextConfig(contextDigester);
19     }

 

其中第18 行的contextConfig方法将完成以下工作:

    • 读取默认的 context.xml文件,如果存在就解析它
    • 读取默认的 Host 配置文件,如果存在就解析它
    • 读取Context自身的配置文件,如果存在就解析它
    • 设置Context的DocBase

ContextConfig 的init方法执行完毕后,Context容器就会执行 容器的 startInternal 方法,由于篇幅限制,就不贴源码了,这个方法主要完成的工作包括:

    • 常见读取资源文件的对象
    • 创建ClassLoader对象
    • 创建应用的工作目录
    • 启动相关的辅助类,比如日志,权限,资源相关的
    • 修改启动的状态,通知web的观察者
    • 子容器的初始化
    • 获取ServletContext并设置必要的参数
    • 初始化web.xml 中 \"load-on-startup\" 的Servlet

三、web应用的初始化工作

  web应用的初始化工作是在ContextConfig 的 configureStart 方法中实现的,应用的初始化主要是解析 web.xml文件,这个文件是描述web应用关键配置文件,也是一个web应用的入口。在configureStart 方法中,调用了 webConfig()方法,该方法会首先寻找 globalWebXml,这个文件的搜索路径是 engine 的工作目录下。或者是 conf/web.xml. 接着找 hostWebXml 。接着寻找应用中WEB-INF/web.xml.在web.xml中的配置项都会被解析成为响应的属性保存在 WebXml对象中。

  调用ContextConfig的configureContext(WebXml webxml)方法,将WebXml 对象中的属性设置到Context容器中,包括Servlet,Filter,Listener。下面是从configureContext 方法中截取的部分源码,详细的描述了如何将一个用户配置的Servlet封装成为一个Context容器的 Wrapper(回想一下文章开头的那一张图)

        for (ServletDef servlet : webxml.getServlets().values()) {
            // 创建一个 Context容器中的 Wrapper
            Wrapper wrapper = context.createWrapper();
            // Description is ignored
            // Display name is ignored
            // Icons are ignored

            // jsp-file gets passed to the JSP Servlet as an init-param

            // 检查是否配置了 LoadOnStartup
            if (servlet.getLoadOnStartup() != null) {
                wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
            }
            // 是否配置了 Enabled
            if (servlet.getEnabled() != null) {
                wrapper.setEnabled(servlet.getEnabled().booleanValue());
            }
            // 将Servlet的名字 封装到 wrapper
            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());
            }
            wrapper.setOverridable(servlet.isOverridable());
            context.addChild(wrapper);
        }

 

  Wrapper是Tomcat容器中的一部分,它具有容器的特征,而Servlet作为一个独立的Web开发标准,不应该耦合在Tomcat找那个,所以需要 Wrapper 对其封装一层,毕竟web容器不只是Tomcat。除了Servlet封装为Wrapper之外,在web.xml中配置的所有属性都会被封装为Wrapper并加载到Context容器中,所以Context容器才是运行Servlet的容器。一个web应用就对应了一个Context容器。而容器中的属性有由web.xml配置。

四、创建Servlet实例

  经过了前面一系列的Servlet解析工作,我们开发的Servlet已经被包装成了Context容器中的Wrapper,但是它任然不能为我们工作,因为它还没有被实例化,下面就来看看它是被如何创建和并初始化的。(毕竟我们写的Servlet是没有main方法的)

创建Servlet对象

  如果Servlet的load-on-startup配置项大于0的话,那么该Servlet在 容器的启动过程中 调用ContextConfig 的 init方法时就被初始化了。其中org.apache.catalina.servlets.DefaultServlet和 org.apache.jasper.servlet.JspServlet,它们的load-on-startup分别是1和3,也就是当Tomcat启动时,这两个Servlet就会启动,这两个Servlet的配置在<TOMCAT_HOME>/conf/web.xml中:

    <servlet>
        <servlet-name>default</servlet-name>
        <servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
        <init-param>
            <param-name>debug</param-name>
            <param-value>0</param-value>
        </init-param>
        <init-param>
            <param-name>listings</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet>
        <servlet-name>jsp</servlet-name>
        <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
        <init-param>
            <param-name>fork</param-name>
            <param-value>false</param-value>
        </init-param>
        <init-param>
            <param-name>xpoweredBy</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>3</load-on-startup>
    </servlet>

 

  而没有配置load-on-startup选项的Servlet,它们的创建方法是从StandardWrapper的loadServlet方法开始的,loadServlet方法的作用主要就是获取ServletClass,然后把它交给InstanceManager去创建一个基于ServletClass.class的对象,如果这个Servlet配置了jsp-file,那么这个Servlet就是默认加载的JspServlet,创建Servlet的相关类结构如图:

[web]Servlet工作原理

初始化Servlet

  Servlet的初始化在StandardWrapper的initServlet方法中,这个方法就是调用了Servlet的init方法,同时将StandardWrapper的 StandardWrapperFacade 作为ServletConfig传递给Servlet,如果该Servlet关联的是一个JSP文件,那么会模拟一次简单请求,目的是将JSP文件编译为Servlet类并初始化,这样Servlet对象就初始化完成了。

五、Servlet体系结构

我们知道Web应用是基于Servlet运转的,那么Servlet 本身又是如何工作呢?它的体系结构是如何的,下面将围绕一张图简单说明Servlet内部的运作:

[web]Servlet工作原理

  Servlet的规范都是基于以上四个类来运转的:ServletContext,ServletConfig,ServletRequest,ServletResponse。其中ServletConfig是初始化时StandardWrapper的 StandardWrapperFacade 作为ServletConfig传递过来的,而ServletRequest,ServletResponse是响应http请求时调用Servlet传递过来的,ServletConfig包含了Servlet相关属性的配置。ServletContext为负责不同模块之间数据交换准备交易场景(全局上下文)。我们在程序中拿到的ServletContext其实是ApplicationContextFacade对象。ApplicationContextFacade对数据起到了封装的作用,保证ServletContext只能拿到该拿的数据,它们之间的设计使用了门面模式, ServletContext可以拿到一些必要的数据,如应用的工作路径,容器支持的Servlet版本等。还有最后一个问题,那就是ServletRequest,ServletResponse为什么在使用的时候可以转换为HttpServletRequest,HttpServletResponse呢?其实他们之间是继承的关系,类似于ServletContext的设计,也是门面设计模式,目的就是为了保证数据的安全。服务器每次收到请求,都是简单解析后快速分配给后续线程处理。

[web]Servlet工作原理

六、Servlet的工作流程

  当用户从浏览器请求http://hostname:port/URL时,hostname:port是用来建立TCP连接的,但是服务器怎么根据这个URL来到达正确的Servlet容器中呢?在Tomcat中,有一个类org.apache.catalina.mapper.Mapper保存了Tomcat Container容器中所有的子容器信息,org.apache.catalina.connector.Request在进入容器之前,Mappper会将这次请求的hostname和contextPath设置到Request对象的mappingData属性中因此,在请求进入之前,就已经知道要访问哪个容器了。下图描述了一个请求如何到达最终的StandardWrapper:

[web]Servlet工作原理

请求到达StandardWrapper后,就要执行Servlet的service方法了,然后根据请求的方式调用doGet或者doPost。

当Servlet从Servlet容器中移除时,也就表明Servlet的生命周期结束了,这时Servlet的destroy方法会被调用,完成一些收尾的工作。

给TA打赏
共{{data.count}}人
人已打赏
随笔日记

分布式事务?咱先弄明白本地事务再说 - 可用性和速度(锁和并发)的博弈

2020-11-9 4:02:42

随笔日记

版本控制工具(SVN/Git)介绍

2020-11-9 4:02:44

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索