概述:互联网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中的一个项目。
二、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接口,该接口管理者整个容器的生命周期,所有容器的修改和状态的改变都将由它去通知已经注册的观察者。该接口中定义的方法如下图:
细心的话我们就会发现,刚才在 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的相关类结构如图:
初始化Servlet
Servlet的初始化在StandardWrapper的initServlet方法中,这个方法就是调用了Servlet的init方法,同时将StandardWrapper的 StandardWrapperFacade 作为ServletConfig传递给Servlet,如果该Servlet关联的是一个JSP文件,那么会模拟一次简单请求,目的是将JSP文件编译为Servlet类并初始化,这样Servlet对象就初始化完成了。
五、Servlet体系结构
我们知道Web应用是基于Servlet运转的,那么Servlet 本身又是如何工作呢?它的体系结构是如何的,下面将围绕一张图简单说明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的设计,也是门面设计模式,目的就是为了保证数据的安全。服务器每次收到请求,都是简单解析后快速分配给后续线程处理。
六、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:
请求到达StandardWrapper后,就要执行Servlet的service方法了,然后根据请求的方式调用doGet或者doPost。
当Servlet从Servlet容器中移除时,也就表明Servlet的生命周期结束了,这时Servlet的destroy方法会被调用,完成一些收尾的工作。