概述: 互联网 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 的方法, 该方法的源码如下, 看看都做了哪些工作?(相关的我在代码中添加了注释)
- public Context addWebapp(Host host, String contextPath, String docBase,
- LifecycleListener config) {
- silence(host, contextPath);
- // 创建一个 StandardContex t 容器, 并设置相关的参数, path, 项目资源路径等
- Context ctx = createContext(host, contextPath);
- ctx.setPath(contextPath);
- ctx.setDocBase(docBase);
- // 是否添加默认的 Web 配置到该项目中
- if (addDefaultWebXmlToWebapp)
- ctx.addLifecycleListener(getDefaultWebXmlListener());
- // 设置 StandardContex 容器的配置文件
- ctx.setConfigFile(getWebappConfigFile(docBase, contextPath));
- // 添加一个监听器到容器中
- ctx.addLifecycleListener(config);
- if (addDefaultWebXmlToWebapp && (config instanceof ContextConfig)) {
- // prevent it from looking ( if it finds one - it'll have dup error )
- // 将传入的参数 LifecycleListener 转为 ContextConfig
- // ContextConfig 将负责整个 Web 应用的配置解析工作
- ((ContextConfig) config).setDefaultWebXml(noDefaultWebXmlPath());
- }
- if (host == null) {
- getHost().addChild(ctx);
- } else {
- host.addChild(ctx);
- }
- return ctx;
- }
将项目添加完成后, 就可以调用 Tomcat 的 start 方法启动了. Tomcat 的启动逻辑是基于观察者模式设计的, 所有的容器都继承了 Lifecycle 接口, 该接口管理者整个容器的生命周期, 所有容器的修改和状态的改变都将由它去通知已经注册的观察者. 该接口中定义的方法如下图:
细心的话我们就会发现, 刚才在 addWebapp 的方法中, 有一个参数就是 LifecycleListener , 它就是容器注册的观察者. 在这里就不仔细深入 Tomcat 的启动过程了, 我们主要关注一下 每一个 Web 应用都会创建的 StandardContex 容器, 它的启动过程到底是怎么样的?
在上面 addWebapp 的源码中, 第 7 行的位置创建了一个 StandardContex 容器, 当 Context 容器的状态为 init 时, 上面源码 19 行添加到 Context 容器的 LifecycleListener 将被调用, 在第 25 行的时候被强转为 ContextConfig, ContextConfig 实现了 LifecycleListener 接口, 负责整个 Web 应用的配置文件解析工作.
ContextConfig 首先调用 init 方法:
- /**
- * Process a "init" event for this Context.
- */
- protected synchronized void init() {
- // Called from StandardContext.init()
- // 创建解析 xml 的 contextDigester
- Digester contextDigester = createContextDigester();
- contextDigester.getParser();
- if (log.isDebugEnabled()) {
- log.debug(sm.getString("contextConfig.init"));
- }
- context.setConfigured(false);
- ok = true;
- // 利用创建的解析器 contextDigester 解析默认的配置文件
- contextConfig(contextDigester);
- }
其中第 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 方法会被调用, 完成一些收尾的工作.
来源: https://www.cnblogs.com/ytuan996/p/10610142.html