性能优化专题共计四个部分,分别是:
本节是性能优化专题第一部分 —— Tomcat性能优化篇,共计三个小节,分别是:
Tomcat官网上,映入眼帘的就是开篇的一句话:
The Apache Tomcat® software is an open source implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies.
ApacheTomcat®软件是Java Servlet,JavaServer Pages,Java Expression Language和Java WebSocket技术的开源实现。
环境选择:
在本专题,Tomcat我们一律采用 8.0.11 版本。
JDK版本:大于等于1.7。
tomcat各个版本下载地址:各个版本产品和源码 【 Download/Archives 】
选择理由:
在tomcat7.0中没有NIO2,在tomcat8.5中没有BIO,而在tomcat8.0中支持的比较丰富
可以在Tomcat8.0.11源码(没有的同学可以参考文末的下载链接)中验证一下: AbstractEndpoint.bind()—>implementation
AbstractEndpoint的bind方法,其实现类包括NIO与BIO,实现类丰富,便于知识点覆盖,所以采用此版本。
在从 Tomcat官网 上下载到Tomcat源码:
然后选择V8.0.11版本:
最后选择src目录:
下载到源码以后,为了方便查阅,我们需要将pom.xml文件集成到根目录,pom文件如下:
<?xml version="1.0" encoding="utf-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" 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">
<modelVersion>4.0.0</modelVersion>
<groupId>org.apache.tomcat</groupId>
<artifactId>Tomcat8.0.11</artifactId>
<name>Tomcat8.0.11</name>
<version>8.0.11</version>
<build>
<finalName>Tomcat8.0.11</finalName>
<!-- 指定源文件为java 、test -->
<sourceDirectory>java</sourceDirectory>
<testSourceDirectory>test</testSourceDirectory>
<resources>
<resource>
<directory>java</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>test</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<encoding>UTF-8</encoding>
<!-- 指定jdk 编译 版本 ,没装jdk 1.7的可以变更为1.6 -->
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<!-- 添加tomcat8 所需jar包依赖 -->
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ant</groupId>
<artifactId>ant</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>wsdl4j</groupId>
<artifactId>wsdl4j</artifactId>
<version>1.6.2</version>
</dependency>
<dependency>
<groupId>javax.xml</groupId>
<artifactId>jaxrpc</artifactId>
<version>1.1</version>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.3</version>
</dependency>
<dependency>
<groupId>org.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.6.1</version>
</dependency>
</dependencies>
</project>
如果上面的过程你觉得没有必要,可以直接找到文末我已经整理好的源码,直接下载即可。
接下来我们使用IDE工具打开源码:
(1)bin:主要用来存放命令,.bat是windows下,.sh是Linux下
(2)conf:主要用来存放tomcat的一些配置文件
(3)lib:存放tomcat依赖的一些jar包
(4)logs:存放tomcat在运行时产生的日志文件
(5)temp:存放运行时产生的临时文件
(6)webapps:存放应用程序
(7)work:存放tomcat运行时编译后的文件,比如JSP编译后的文件
这块咱们就不详细去说了,因为在Javaweb中都学过,即使忘了一些文件或者文件夹的作用,网上介绍的一大堆~
One More Thing,分析Tomcat源码之前,请允许我现在站在上帝视角,以极其简短的代码概括Tomcat8.0的核心功能,我将其称之为Tomcat 8.0 Mini!
这里有人要问了,Tomcat到底是做什么的?核心功能是什么?
实际上Tomcat是一个web服务器,说白了就是能够让客户端和服务端进行交互。比如客户端想要获取服务端某些资源,服务端可以通过tomcat去进行一些处理并且返回。
为什么要手写?既然上述提到了tomcat是java语言写的,又和servlet相关,那就自己设计一个试试,先不管作者的想法如何 。
基于Socket进行网络通信
//基于网络编程socket套接字来做
class MyTomcat{
ServerSocket server = new ServerSocket(8080);
Socket socket = server.accept();
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
}
好了,这就是mini版tomcat,实际上就是通过serversocket在服务端监听一个端口,等待客户端的连接,然后能够获取到对应的输入输出流。
是不是没过瘾,接下来我们对其做一个升级
//优化1:将输入输出流封装到对象
class MyTomcat{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
InputStream in=socket.getInputStream();
new Request(in);
OutputStream out=socket.getOutputStream();
new Response(out);
}
class Request{
private String host;
private String accept-language;
}
class Response{
}
发现一个比较靠谱的tomcat已经被我们写出来了,问题是这个tomcat如果使用起来方便吗?你会发现不方便,因为对应的request和response都放到了tomcat源码的内部,业务人员想要进行开发时,很难获得request对象,从而获得客户端传来的数据,也不能进行很好的返回,怎么办呢?
我们发现在JavaEE中有servlet这项技术,比如我们进行登录功能业务代码开发时,写过如下这段代码和配置
servlet:
package com.test.web.servlet.SimpleServlet;
//优化2前奏:
class LoginServlet extends HttpServlet{
doGet(request,response){
}
doPost(request,response){
}
}
web.xml配置:
<servlet>
<servlet-name>LoginServletservlet-name>
<servlet-class>com.test.web.servlet.SimpleServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>LoginServletservlet-name>
<url-pattern>/loginurl-pattern>
servlet-mapping>
所以我们现在通过集成HttpServlet获取到request与response对象后再做调整:
//优化2:
class MyTomcat{
List list = new ArrayList();
ServerSocket server = new ServerSocket(8080);
Socket socket = server.accept();
//也就是这个地方不是直接处理request和response
//而是处理一个个servlets
list.add(servlets);
}
换句话说:tomcat官方开发者对于用list集合保存项目中的servlets也是这样想的吗?我们可以从几个维度进行一下推测
业务代码中关于servlet想必大家都配置过,或者用注解的方式,原本开发web应用就采用的是这样的方式。你的controller中有很多自己写的servlet,都继承了HttpServlet类,然后web.xml文件中配置过所有的servlets,也就是mapping映射,这个很简单。
如果apache提供的tomcat也这么做了,势必也要跟servlet规范有关系,也就是要依赖servlet的jar包,我们来看一下在tomcat产品的文件夹之下有没有servlet.jar,发现有。
最后我们如果能够在tomcat源码中找到载入servlets的依据,就更加能说明问题了
于是我们在idea中的tomcat8.0源码,关键是到哪里找呢?总得有个入口吧?源码中除了能够看到各种Java类型的文件之外,一脸懵逼,怎么办?
不妨先跳出来想想,如果我们是tomcat源码的设计者,也就是上述手写的代码,我们怎么将业务代码中的servlets加载到源码中?我觉得可以分为两步
(1)加载web项目中的web.xml文件,解析这个文件中的servlet标签,将其变成java中的对象
(2)在源码中用集合保存
注意第(1)步,为什么是加载web.xml文件呢?因为要想加载servlets,一定是以web项目为单位的,而一个web项目中有多少个servlet类,是会配置在web.xml文件中的。
寻找和验证,加载和解析web.xml文件
加载 :ContextConfig.webConfig()—>getContextWebXmlSource()—>Constants.ApplicationWebXml
解析 :org.apache.catalina.startup.ContextConfig.webConfig()—>configureContext(webXml)—>context.createWrapper()
将servlets加载到list集合中
org.apache.catalina.core.StandardContext.loadOnStartup(Container children[])—>list.add(wrapper)
怎么知道上面找的过程的?
我们会发现上面加载web.xml文件和添加servlets都和Context有点关系,因为都有这个单词,那这个Context大家眼熟吗?其实我们见过,比如你把web项目想要供外界访问时,你会添加web项目到webapps目录,这是tomcat的规定,除此之外,还可以在conf/server.xml文件中配置Context标签。
按照经验之谈,一般框架的设计者都会提供一两个核心配置文件给我们,比如server.xml就是tomcat提供给我们的,而这些文件中的标签属性最终会对应到源码的类和属性。
换句话说:tomcat官方开发者对于监听端口也是这么设计的吗
其实我们手写的tomcat这块有两个核心:第一是监听端口,第二是添加servlets,上面解决了添加servlets。
接下来显然我们有必要验证一下监听端口tomcat也是这么做的吗?
org.apache.catalina.connector.Connector.initInternal()->protocolHandler.init()
->AbstractProtocol.init()
->endpoint.init()->bind()
这里的bind()的实现方式,就是我们开篇提到的Socket实现方式,包括Apr,JIo,NIO,NIO2这四种方式,正因为实现类丰富,我们才使用了Tomcat8.0.11版本。
为什么知道找Connector?
再次回到conf/web.xml文件,发现有一个Connector标签,而且还可以配置port端口,我们能够联想到监听端口,按照配置文件到源码类的经验,源码中一定会有这样一个类用于端口的监听。
在tomcat这块左边一定会监听在某个端口,等待客户端的连接,不然所有的操作都没办法进行交互
里面的模块,Engine,Host等模块可依据配置文件web.xml抽丝剥茧,不难分析出来。
而我们通过官网查到的Tomcat架构图也印证了我们的猜想:
从上面的Tomcat架构图中我们可以知道,不同的组件分工不同,用以实现客户端与服务端之间的交互。那么接下来,我们通过官方文档进一步加深对于不同组件的理解,毕竟,学习一项开源技术,官方网站相对比较权威。
官网访问步骤:Documentation/Tomcat8.0/Development/Architecture/Overview
最终打开页面:http://tomcat.apache.org/tomcat-8.0-doc/architecture/overview.html
换句话说:之前找了两个点,监听端口,加载servlets的调用过程是如何的?
比如bind(), loadOnstartup()到底谁来调用?
此时大家还是要回归到最初的流程,客户端发起请求到得到响应来看。
客户端角度:发起请求,最终得到响应
tomcat代码角度 :虽然是要监听端口和添加servlets进来,但是肯定有一个主函数,从主函数开始调用
说白了,如果我是源码设计者,既然架构图我都了解了,肯定是要把这些组件初始化出来,然后让它们一起工作,也就是:
初始化一个个组件
利用这些组件进行相应的操作
我们总说看别人的代码是一种痛苦,尤其是源码。很大的一个原因就在于我们不了解整体架构,不知道作者这样表达的含义在哪里?
更何况一款优秀的开源框架往往都是一个团队花费大量时间与精力,精雕细琢出来的。在我们不了解逻辑的情况下,盲目查看,不自觉之间就会掉入设计模式的汪洋大海里。久之,则失去了对于源码探索的勇气,多了一份对于其的恐惧感,而往往对于源码的理解则是程序员的分水岭!
那么我们如何去学习源码以提高认知呢?
好的,闲聊结束,开始正文,建议同学们下载好源码,与我一起跟着步骤依次翻阅,这样理解更深!
一定有一个类,这个类中有main函数开始,这样才能有一款java源码到产品,一贯的作风。感性的认知: org.apache.catalina.startup.BootStrap ->main()->根据脚本命令->startd
果然被我们找到了,先加载再启动,那就继续看咯
类比推理,不难得出,daemon.load() 为加载过程,daemon.start()启动过程…
Bootstrap.main()->Bootstrap.load()
紧接着进入Bootstrap.load()方法:
这里的method.invoke 执行的方法是什么呢?实际上在Catalina.init()方法里就对method做了赋值,即method为org.apache.catalina.startup.Catalina,而在load()方法里,则指定了Catalina类里具体的方法以及构造参数,所以Catalina.load()这里最终会带着指定参数执行Catalina.load()方法。
现在看->Catalina.load(),有一段关键内容,即 start the new server:
初始化的依据是什么?考虑coder的设计server.xml,即加载server.xml文件,初始化一些组件。
于是我们看getServer().init(),发现会跳转到Lifecycle.init()
->LifecycleBase.init()->LifecycleBase.initInternal()
LifecycleBase.init()有三个实现类,这里我们选择默认实现,即LifecycleBase.initInternal()
->StandardServer.initInternal()
在我们前面提到的Tomcat架构图里我们说,Server组件是最外层的部分,所以这里选择StandardServer。记得这里是一个十字路口,因为会初始化很多东西,后面还对多次提到不同的初始化内容。
->services[i].init()
参考Tomcat架构图,不难发现,在Server层里面,Service组件是可以存在多个的,所以这里进行循环遍历,依次初始化。
->StandardService.initInternal()
回过头来到十字路口,再看其他组件初始化过程,现在我们看Service初始化:
->executor.init()/ connector.init()
executor.init()这里初始化以后,发现又回回到初始化默认实现LifecyleBase.initInternal()方法
->LifecyleBase.initInternal()
既然又回到十字路口,我们依据Tomcat架构图看看Connector的初始化
->Connector.initInternal()
->protocolHandler.init()
这里我们选择AbstractProtocol
->AbstractProtocol.init()
->endpoint.init()->bind()
又回到最初的起点,前面已提过这里的Socket实现。
->Apr,JIo,NIO,NIO2
之所以采用多种IO操作,其目的就在于适用于不同的场景,IO的不同,其性能是不同的。
conclusion:请求目前没有来,只是内部的初始化工作
前面我们通过daemon.load()方法,主要对Tomcat中Server、Service、Connector组件进行了初始化,那么关于Engine,Host,Context,Wrapper等这些组件却没有提及,回过头来我们看Bootstrap启动startd
的过程中除了daemon.load(),剩下的就是daemon.start()。
Bootstrap.start()->Catalina.start()
同load()过程,这里使用method.invoke,直接跳转到Catalina.start()
->getServer.start()->LifecycleBase.start()
同load()过程,这里选择LifecycleBase默认初始化方法
->LifecycleBase.startInternal()
->StandardServer.startInternal()
->services[i].start()
同load()过程,循环遍历service,分别初始化
->StandardService.startInternal()
同load()过程,选择LifecycleBase,然后继续初始化其他组件
这里我们进入
container.start();
-> container.start()/executors.init()/connectors.start()
查看一下Container接口,发现Engine,Host,Context,Wrapper等都是他的实现类。那么 container.start(),实际上就是对于这些子类进行初始化。
回过头来我们进入container.start()方法:
同上,这里依旧选择默认:
再次进入初始化startInternal()方法。
->engine.start()
现在进入最外层的组件,选择StandardEngine
->StandardEngine.startInternal()
查看一下StandardEngine类关系结构图,发现ContainerBase是它的爸爸,而这个爸爸有多少孩子呢?
Engine,Host,Context,Wrapper都是它的孩子
->super[ContainerBase].startInternal()->代码呈现
//调用Engine子容器的start方法
results.add(startStopExecutor.submit(new StartChild(children[i])))
那子容器是什么呢?可能就是Engine,Host,Context,Wrapper等。
我们继续看线程池提交任务的过程都做了什么事情?
注意child.start,那么这里的child是谁呢?可能就是Engine,Host,Context,Wrapper等。
似曾相识吧,这里我们再次轮询上面的过程。
->LifecycleBase.start()
->startInternal()
->StandardHost.startInternal()
以Host为例,这个过程就完成了Host组件初始化, Host将一个个web项目加载进来:
StandardHost.startInternal()
->ContainerBase.startInternal()
通过不断的循环子类放入到线程池的过程(责任链模式),就完成了对于Engine,最后threadStart()。
->threadStart()
protected void threadStart() {
if (thread != null)
return;
if (backgroundProcessorDelay <= 0)
return;
threadDone = false;
String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
//核心实现
thread = new Thread(new ContainerBackgroundProcessor(), threadName);
thread.setDaemon(true);
thread.start();
}
我们看new Thread后重写run方法,都做了什么事情,所以我们看初始化ContainerBackgroundProcessor的过程
->container.backgroundProcess()
老套路,进入默认实现ContainerBase
->ContainerBase.backgroundProcess()
->fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null)
->listener.lifecycleEvent(event)->interested[i].lifecycleEvent(event)
->监听器HostConfig->HostConfig.lifecycleEvent(LifecycleEvent event)
->check()-> deployApps()
deployApps顾名思义,发布APP
protected void deployApps() {
File appBase = host.getAppBaseFile();
File configBase = host.getConfigBaseFile();
String[] filteredAppPaths = filterAppPaths(appBase.list());
// Deploy XML descriptors from configBase
deployDescriptors(configBase, configBase.list());
// Deploy WARs
deployWARs(appBase, filteredAppPaths);
// Deploy expanded folders
deployDirectories(appBase, filteredAppPaths);
}
回到 StandardHost.startInternal(),回到最初的起点,我们看看Context组件初始化过程:
-> results.add(startStopExecutor.submit(new StartChild(children[i])));
然后又会调用它的子容器
-> super.startInternal()
-> StandardContext.initInternal()
解析每个web项目
-> StandardContext.startInternal()
-> fireLifecycleEvent(Lifecycle.CONFIGURE_START_EVENT, null)
-> listener.lifecycleEvent(event)
-> interested[i].lifecycleEvent(event)
->[找实现]ContextConfig.lifecycleEvent(LifecycleEvent event)
-> configureStart()
-> webConfig()
解析每个web项目的xml文件了
-> getContextWebXmlSource()
-> Constants.ApplicationWebXml
这就回到了刚才我们提过的
public static final String ApplicationWebXml = "/WEB-INF/web.xml";
通过ContextConfig.webConfig()加载配置后,解析元素到servlets包装成wrapper对象。
何时调用loadOnstartup()?
在前面的Tomcat初始化组件的过程,我们也可以从官网的UML时序图加以认证。
关于Startup官网访问步骤:
Documentation/Tomcat8.0/Apache Tomcat Development/Architecture/Server Startup
Server Startup
关于Process官网访问步骤:
Documentation/Tomcat8.0/Apache Tomcat Development/Architecture/Request Process
UML sequence diagram
Tomcat 8.0.11 源码地址:
https://github.com/harrypottry/apache-tomcat-8.0.11-src
更多架构知识,欢迎关注本套系列文章:Java架构师成长之路