Servlet是 JavaWeb 开发中最常使用的一个接口,尤其是这个接口中的 doGet()和 doPost()方法。我们在做 web 开发的时候,经常会自定义一个 Servlet 如 HelloServlet,并且让这个类继承 HttpServelt,接着重写 doGet()方法就可以快速实现我们自己的请求服务。
那么 doGet()方法的背后到底发生了什么?有些同学可能会说这个问题很简单啊,就是HttpServlet 做了一次封装会判断 HTTP 请求的类型,如果是 get 请求就调用 doGet()方法,如果是 post 请求就调用 doPost()方法。
我们想要的并非这种简单的回答,而是探究这背后的背后究竟发生了什么?
HelloServlet ->HttpServlet->ApplicationFilterChain -> WsFilter -> StandardWrapperValve ->StandardContextValve -> StandardHostValve -> StandardEngineValve ->CoyoteAdapter -> Http11Processor -> NioEndpoint -> ThreadPoolExecutor-> Worker -> TaskThread -> Thread -> Catalina ->Bootstrap.main()
这才是最终我们想要得到的答案,从 doGet 方法开始,逐步的探究它开始的地方,最终这个开始的地方在什么地方结束呢?答案无疑是 Tomcat 程序启动的入口 main 函数。只有完成了这样的一个历程,我们才能说我们彻底明白了 doGet()方法,彻底明白了这背后到底发送了什么。
通过本系列博客的阅读,您将彻底的了解 doGet()方法背后发生了什么,从源码的角度深入的理解 Tomcat 的实现机制,Tomcat 中各核心组件是如何协同工作的,同时也会学习到 WEB 服务器设计思路。
1 目标
本系列博客源码分析的目标是深入了解Tomcat中doGet方法的实现机制。本次源码分析的目标是了解 Servlet。
2 分析方法
首先编写测试代码,然后利用 Intellij Idea 的堆栈窗口、线程窗口以及单步调试功能,逐步的分析其实现思路。
前期准备工作如下:
1) 编写 HelloServlet类。
public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequestreq, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("hello");//断点位置 } }
2) web.xml 中添加servlet配置。
<servlet> <servlet-name>helloServlet</servlet-name> <servlet-class>HelloServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>helloServlet</servlet-name> <url-pattern>/hello</url-pattern> </servlet-mapping>
3)测试运行
浏览器地址栏输入:http://localhost:8080/hello
最终页面显示结果:hello
4)进入调试分析阶段。
在 HelloServlet 的 doGet()方法前加上断点,点击调试按钮,进入调试阶段,开始源码分析。
3 分析流程
点击调试按钮,开始分析流程。
首先我们来看一下 doGet()方法的执行堆栈信息。
- at HelloServlet.doGet(HelloServlet.java:17)
- at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
- at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
- at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
- at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
- at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
- at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
- at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
- at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
- at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
- at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:475)
- at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:140)
- at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:80)
- at org.apache.catalina.valves.AbstractAccessLogValve.invoke(AbstractAccessLogValve.java:624)
- at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:87)
- at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:342)
- at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:498)
- at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
- at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:796)
- at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1368)
- at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
- at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
- at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
- at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
- at java.lang.Thread.run(Thread.java:748)
从上面我们可以看到一共有25条堆栈信息,调用非常复杂,但这25条信息也并非是程序开始的地方,后面我们会带大家一起来寻找程序开始的地方。
我们要想搞明白 doGet()方法背后发生了什么,其实就是要弄明白这些堆栈信息,所以目前我们看到的就有25条信息,后面将一步步的带大家来了解。
由于分析内容较多,我们将分几次博客来介绍,如果您对本系列博客感兴趣,欢迎关注微信公众号“算法与编程之美”,及时了解更多信息。
首先对这些堆栈信息做一些简单的介绍,
HelloServlet.doGet(HelloServlet.java:17)
表示HelloServlet 类的 doGet()方法,代码行数为17.
3.1 HelloServlet.doGet
@Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.getWriter().write("hello"); //17 }
这里面的17行就是我们打断点的地方,也是我们用户编写程序开始的地方。功能很简单就是向HTTP 响应中写入"hello"字符串。
3.2 javax.servlet.http.HttpServlet.service
2. at javax.servlet.http.HttpServlet.service(HttpServlet.java:635)
3. at javax.servlet.http.HttpServlet.service(HttpServlet.java:742)
2和3我们打算一起来分析,因为他们都在 HttpServlet 里面。在具体分析源码之前,我们先介绍一点关于 Servlet 的基本知识。
在学习Servlet 基础知识的时候,我们都知道,Servelt 是一段服务器端的程序,专门用来处理来自客户端请求的,处理完成后返回一个响应。大多数情况下这个请求是 Http 请求,这个响应是 Http响应,并且在Http 响应中包含了 HTML 代码。
Servlet是一个接口,其定义如下:
public interface Servlet { public void init(ServletConfig config) throws ServletException; public ServletConfig getServletConfig(); public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException; public String getServletInfo(); public void destroy(); }
这个接口定义非常简单,主要方法介绍如下:
init方法表示 Tomcat 服务器刚加载这个 Servlet 时执行的代码。
service方法表示这个 Servlet 在处理请求时执行的代码。
destroy 方法表示当不再使用这个 Servelt 销毁时执行的代码。
所以我们看到 Servlet 是有生命周期的,刚开始诞生的时候调用 init 方法,期间服务请求时,调用 Service 方法,最后销毁时调用 destroy 方法。
这里面尤其需要注意的是 service()方法的两个形参类型是 ServeltRequest和 ServletResponse。
由于我们在 web.xml 中配置了将访问路径为'/hello'的请求交给 HelloServlet 来处理,此外由 HelloServlet 的如下的继承关系:
- HelloServlet 继承了HttpServlet
public class HelloServlet extends HttpServlet
- HttpServlet继承了 GenericServlet
public abstract class HttpServlet extends GenericServlet
- GenericServlet 实现了 Servlet 接口
public abstract class GenericServlet implements Servlet, ServletConfig, java.io.Serializable
我们可以得出 HelloServlet 是一个符合 JavaEE 规范的 Servlet,因此能够处理Http 请求,按照上述介绍的理论,当Http 请求'/hello'到达服务器时,将调用 HelloServlet 的 service 方法对其进行处理。
HelloServlet中并没有 service 方法,该方法位于其父类 HttpServlet 中,其定义如下所示:
public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { HttpServletRequest request; HttpServletResponse response; try { request = (HttpServletRequest)req; response = (HttpServletResponse)res; } catch (ClassCastException var6) { throw new ServletException("non-HTTP request or response"); } this.service(request, response); }
从上述代码可以看到,将ServletRequest强制转化为 HttpServletRequest,将 ServletResponse 强制转化为 HttpServletResponse,然后再交给另外一个 service()方法处理。
为什么要做这种转化呢?为什么不直接处理 ServletRequest和 ServletResponse?
欢迎大家留言,说说您的看法。
做完这种类型转化后,交给另一个 service 方法处理。
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String method = req.getMethod(); if (method.equals(METHOD_GET)) { // ... doGet(req, resp); } else if (method.equals(METHOD_POST)) { doPost(req, resp); } else if (method.equals(METHOD_PUT)) { // ... }
以上的代码大家应该都非常容易理解,首先获得 Http 请求方法的类型,然后根据不同的类型去调用不同的方法,如方法类型为 get 则调用 doGet()方法。由于在子类 HelloServlet 实现了 doGet()方法,因此最终执行的是上面我们写的代码。
通过§3.1 和§3.2两节的分析我们知道,当一个请求达到 Servlet 的时候,首先会将这个请求转化为HttpServletRequest,这个响应转化为 HttpServletResponse,然后得到 Http 请求的方法类型,最后根据不同的方法类型调用不同的方法来处理。
4 总结
本文是《Tomcat 源码分析之 doGet方法》的第一篇文章,主要介绍了源码分析的目标以及主要任务有哪些,并对Servlet知识点做了非常细致的介绍,帮助大家更好的了解 Servlet,以及为什么用户自定义的 Servlet 需要继承 HttpServlet。
下一讲我们将介绍4、5、6、7、8,重点介绍 ApplicationFilterChain 的相关知识点,欢迎大家持续关注。