周末两天时间,太无聊了。于是我就在想作为一个优秀的程序开发者,我们不仅需要对自己编写的程序有深刻的理解和认识,同样的我们还需要对我们编写的程序的运行环境有一个彻底的认识,这样我们在能在这条路上走的更远。所以我用了这两天的时间亲自撸了一边tomcat的源码,下面和大家一起分享本次的成果。
相信大家对tomcat都很熟悉,目前也是中国大陆使用的较多的一种开源的Web容器。对于Tomcat的基本使用这里不做过多的介绍。具体的下载和配置可以查看我在2018年写的https://blog.csdn.net/qq_38701478/article/details/85003063 这篇文章。
好了我们进入今天的主题,首先准备好源码,这里使用的版本是tomcat8.5-42的版本。关于源码的下载大家可以去官网(http://tomcat.apache.org/)下载 。下载完成之后解压源码包,如下图所示:
接下来我们进入到源码解压的路径下,创建一个home目录,并且将conf和webapps目录移动到home目录中,如下图所示 :
home目录中:
接下来 我们就开始使用maven来构建一把tomcat的源码 ,首先我们在apache-tomcat-8.5.42-src目录下创建一个pom.xml文件,该文件中加入tomcat所需要的依赖,如下所示:
<?xml version="1.0" encoding="UTF-8"?> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.apache.tomcat apache-tomcat-8.5.42-src Tomcat8.5 8.5 Tomcat8.5 java java org.apache.maven.plugins maven-compiler-plugin 2.3 UTF-8 1.8 1.8 junit junit 4.12 test org.easymock easymock 3.4 ant ant 1.7.0 wsdl4j wsdl4j 1.6.2 javax.xml jaxrpc 1.1 org.eclipse.jdt.core.compiler ecj 4.5.1
好了,接下来 我么打开idea,选择导入源码项目 ,按以下步骤导入即可:
我们选中pom文件 点击ok即可将源码导入到idea中 ,如下图所示:
我们点击最左边的project, 就可以看到tomcat源码的结构 如下图:
我们都知道Java程序都有一个入口,也就是我们常说的main方法 ,好了我们直接来全局搜索一下main方法的位置似乎和我们像的不太一样,emmm.........好趴,想想其他的办法 我们平时在启动tomcat的时候都是执行bin目录下的startup.sh文件(windows下是startup.bat文件)。那么我们就来看看 这个文件吧,看看能不能发现什么有用的东西。
我们在这个文件中没有发现什么有用的信息,但是 在第56行发现了该文件执行了catalina.bat这个脚本 ,好吧 我们继续来查看catalina.bat这个脚本。
功夫不负有心人 我们在第257行发现了有一个可疑的地方MAINCLASS这个变量的值是org.apache.catalina.startup.Bootstrap。看起来这个就是启动类啊。我们打开这个类瞅一眼:
果然我们发现了熟悉的main方法。好吧 我们先来执行一把
我么发现程序启动报错了,原因是找不到配置文件server.xml。好吧 我们来编辑一下启动需要的配置,按以下步骤操作:在虚拟机参数选项栏中加入以下配置即可:
-Dcatalina.home=D:/projec/wxzhh_src/apache-tomcat-8.5.42-src/home-Dcatalina.base=D:/projec/wxzhh_src/apache-tomcat-8.5.42-src/home-Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager-Djava.util.logging.config.file=D:/projec/wxzhh_src/apache-tomcat-8.5.42-src/home/conf/logging.properties
这里的路径根据自己的实际情况修改 我存放源码的路径是D:/projec/wxzhh_src。好了 我们再来启动一把试试:
我们发现好像启动成功了,熟悉的8080端口已经在监听的状态了,于是我们打开浏览器 访问一下试试:
错误来的很突然,突然报500了,还是我们熟悉的空指针异常!Unable to compile class for JSP。不能编译JSP文件 ,好吧 肯定是JSP解析器出问题了,书上给的解释是是我们直接启动org.apache.catalina.startup.Bootstrap的时候没有加载JasperInitializer,从而无法编译JSP。解决办法是在tomcat的源码ContextConfig中的configureStart函数中手动将JSP解析器初始化
我们在webConfig();方法之前加上一行代码即可:
context.addServletContainerInitializer(new JasperInitializer(), null);
好吧 我们再继续启动项目,然后再打开浏览器访问试试
OK了!!我们成功的构建了一把Tomcat的源码,下面我们就来看看Tomcat的项目架构。 首先我们都知道,tomcat是一种web容器,主要用来发布我们的Web项目,用户可以通过Http请求来访问我们发布的项目。关于HTTP协议这里不做过多的介绍,相信大家也很熟悉,我们重点介绍Servlet容器。首先我们来看这张图在Tomcat 内部逻辑上主要是有两个部分组成,一部分是接受客户端的HTTP请求的HTTP服务器,另一部分是处理Servlet的Servlet容器,HTTP服务器不直接处理业务请求,而是把请求转发给Servlet容器来进行处理,Servlet容器通过接口来调用实际的业务代码来完成用户的请求。 这样设计的好处就是达到了HTTP服务器和具体以的业务代码解耦的目的。
我们都知道Servlet容器和Servlet接口共同组成了一套规范,叫做Servlet规范,在tomcat内部对这个规范做了具体的实现,同时还让他们具备了HTTP服务器的功能。
我们可以回忆一下,在学习JavaWeb的时候,我们要实现某种业务功能,只需要实现一个Servlet,并把它注册Tomcat(Servlet容器)中,最后tomcat就可以帮我们完成剩下的工作了,现在是不是知道了原因了
接下来我们再来看看tomcat的源码,上面我们已经到了Tomcat 的主启动类,我们来看一下main方法里面的内容
public static void main(String args[]) { if (daemon == null) { // Don't set daemon until init() has completed Bootstrap bootstrap = new Bootstrap(); try { bootstrap.init(); } catch (Throwable t) { handleThrowable(t); t.printStackTrace(); return; } daemon = bootstrap; } else { // When running as a service the call to stop will be on a new // thread so make sure the correct class loader is used to prevent // a range of class not found exceptions. Thread.currentThread().setContextClassLoader(daemon.catalinaLoader); } try { String command = "start"; if (args.length > 0) { command = args[args.length - 1]; } if (command.equals("startd")) { args[args.length - 1] = "start"; daemon.load(args); daemon.start(); } else if (command.equals("stopd")) { args[args.length - 1] = "stop"; daemon.stop(); } else if (command.equals("start")) { daemon.setAwait(true); daemon.load(args); daemon.start(); if (null == daemon.getServer()) { System.exit(1); } } else if (command.equals("stop")) { daemon.stopServer(args); } else if (command.equals("configtest")) { daemon.load(args); if (null == daemon.getServer()) { System.exit(1); } System.exit(0); } else { log.warn("Bootstrap: command \"" + command + "\" does not exist."); } } catch (Throwable t) { // Unwrap the Exception for clearer error reporting if (t instanceof InvocationTargetException && t.getCause() != null) { t = t.getCause(); } handleThrowable(t); t.printStackTrace(); System.exit(1); } }
我们发现上述代码中有一行是bootstrap.init(); 这里我们可以跟踪进去看一下。代码如下 :
public void init() throws Exception { initClassLoaders(); Thread.currentThread().setContextClassLoader(catalinaLoader); SecurityClassLoad.securityClassLoad(catalinaLoader); // Load our startup class and call its process() method if (log.isDebugEnabled()) log.debug("Loading startup class"); Class> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina"); Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader if (log.isDebugEnabled()) log.debug("Setting startup class properties"); String methodName = "setParentClassLoader"; Class> paramTypes[] = new Class[1]; paramTypes[0] = Class.forName("java.lang.ClassLoader"); Object paramValues[] = new Object[1]; paramValues[0] = sharedLoader; Method method = startupInstance.getClass().getMethod(methodName, paramTypes); method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance; }
我们发现这个方法其实是加载了 org.apache.catalina.startup.Catalina这个类,这里我们可以理解成bootstrap.init();方法其实就是在初始化org.apache.catalina.startup.Catalina这个类。也就是我们在启动tomcat的时候经常看到的日志信息中的那个catalina。
这里先给大家介绍一下Catalina这个对象,也被称为Catalina容器。其实大家应该也隐隐的感觉到了,之前给大家介绍的Servlet容器的名字就是叫Catalina。下面我们来看一下这张架构图:
上图就是Tomcat 的完整的架构,这里先简单说一下上图中每个组件的作用首先Catalina容器主要是调用业务代码,Coyote是一个连接器,主要负责通讯功能,Jsaper是JSP的解析工具,JavaEL用来解析服务端的表达式语言、Naming 提供JNDI 服务,Juli 提供日志服务。我们在使用tomcat的功能的时候其实都是被由上述组件共同完成的。
这里我们先来说说 Coyote连接器的作用,首先来看一下下面这张架构图
Coyote连接器就是tomcat对(外部)客户端提供的访问接口,客户端通过Coyote与服务器建立连接、发送请求并接受响应 。上图就是Coyote连接器的工作模式。Coyote 封装了底层的网络通信(Socket 请求及响应处理),为Catalina 容器提供了统一的接口,使Catalina 容器与具体的请求协议及IO操作方式完全解耦。Coyote 将Socket 输入转换封装为 Request 对象,交由Catalina 容器进行处理,处理请求完成后, Catalina 通过Coyote 提供的Response 对象将结果写入输出流 。
作为独立的模块,Coyote只负责具体的协议以及IO相关的操作。与Servlet 规范实现没有直接关系,因此即便是 Request 和 Response 对象也并未实现Servlet规范对应的接口, 而是在Catalina 中将他们进一步封装为ServletRequest 和 ServletResponse
好了,提到IO和协议这里也简单的说明一下Tomcat内部支持的一些协议和IO模型吧,如下图所示:
需要注意的是在 8.0 之前 , Tomcat 默认采用的I/O方式为 BIO , 之后改为 NIO。无论 NIO、NIO2 还是 APR, 在性能方面均优于以往的BIO。如果采用APR, 甚至可以达到 Apache HTTP Server 的性能。
下面来看一下连接器的组件,如下图所示:
我们可以看到EndPoint 的作用就是负责底层的通信,他是具体Socket接收和发送处理器,是对传输层的抽象,因此EndPoint用来实现TCP/IP协议的。至于Processor, 它是Coyote 协议处理接口 ,用来实现HTTP协议,Processor接收来自EndPoint的Socket,读取字节流解析成Tomcat Request和Response对象,并通过Adapter将其提交到容器处理,Processor是对应用层协议的抽象。 最后 Adapter的作用就是 将便是 Request 和 Response 对象进一步封装为ServletRequest 和 ServletResponse。
下面copy书上的一句话来总结一下:
好了,最后给大家介绍一下Jasper引擎,Jasper模块是Tomcat的JSP核心引擎,我们知道JSP本质上是一个Servlet。Tomcat使用Jasper对JSP语法进行解析,生成Servlet并生成Class字节码,用户在进行访问jsp时,会访问Servlet,最终将访问的结果直接响应在浏览器端。如下图所示:
我们可以发现,在tomcat的web.xml文件中配置了一个org.apache.jasper.servlet.JspServlet,他就是用于处理所有的.jsp 或 .jspx 结尾的请求,该Servlet 实现即是运行时编译的入口。 将jsp文件编译成Java字节码然后再有jvm解释执行。
好了今天关于tomcat的组件就给大家介绍这么多了,晚安!!