Tomcat是一款大家非常熟悉的web服务器,具体的功能和怎么使用我就不赘述咯,今天我们主要来分析Tomcat的源码,基于的版本是8.0.11。问题是从哪边开始着手呢?我是这样想的,肯定是先把Tomcat8.0.11的源码在官网中下载下来,然后导入到工程中。
1 Tomcat源码下载及导入工程
1.1 源码下载
Tomcat版本:Tomcat8.0.11
下载地址:点击下载
然后选择对应的平台即可
1.2 pom文件准备及导入工程
下载好了之后解压,我是基于windows平台的,我们可以基于maven的方式进行构建,所以需要引入pom.xml文件相关的依赖,下面是我给大家准备的pom.xml文件,大家可以直接使用,放到源码的根目录下。
<?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</artifactId>
<name>Tomcat8.0</name>
<version>8.0</version>
<build>
<finalName>Tomcat8.0</finalName>
<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</version>
<configuration>
<encoding>UTF-8</encoding>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.4</version>
</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.eclipse.jdt.core.compiler</groupId>
<artifactId>ecj</artifactId>
<version>4.5.1</version>
</dependency>
</dependencies>
</project>
好了之后,可以将源码导入到工程中,这里我使用的是idea,导入完成之后如下图所示。
接下来就是分析源码吗?别急,现在分析也不会有任何思路,完全一脸懵逼,所以我们得想想该如何入手。看了看这些文件,我们能够得出的结论是,发现Tomcat是Java语言开发的,其实这个可能大家一开始就知道,没什么了不起的。既然是Java语言开发的,我们又是Java开发人员,不妨先自己手写一个Tomcat,然后看根据手写的思路再去推导源码的实现方式,这样不就无缝结合了吗?ok,那就手写咯。
2 手写Tomcat
手写之前先想想Tomcat的作用是什么?是web服务器,它是给客户端提供访问并且返回给客户端资源的,此时得要写一段代码能够让客户端连接上来,然后再进行数据交互操作。怎样让客户端连接上来?用Java中已有的知识可以采用Socket编程。
2.1 ServerSocket监听端口等待客户端连接
class MyTomcat{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
}
2.2 根据客户端的连接处理数据交互
class MyTomcat{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
//接受客户端传来的数据
InputStream in=socket.getInputstream();
//进行相应的业务处理
......
//返回给客户端数据
OutputStream out=socket.getOutputStream();
}
至此,通过Socket编程和IO一个mini版的Tomcat就完成了,关键是我们得想想是否可以优化。
2.3 优化Tomcat
- Request和Response优化
了解过http协议的小伙伴都知道,http传来的请求有请求头,请求行,请求体这些,最主要的是哪些键值对数据,既然Java是一门面向对象的开发语言,不妨把这些数据封装成Java中的对象,于是我们声明一个Request类,如下所示。
class Request{
private String accept;
private String host;
priavte ...
}
这些属性和http请求中的数据时一一对应的,有了request之后,不妨把response也封装一下咯。
class Response{
private String Content-Type;
private Date date;
private ...
}
这时候http中的请求和响应和Java中的Request和Response对象已经对应起来了,所以代码可以优化成如下所示。
class MyTomcat{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
//接受客户端传来的数据
InputStream in=socket.getInputstream();
//将输入流封装成Request对象
Request request=new Request(in);
//进行相应的业务处理
......
//返回给客户端数据
OutputStream out=socket.getOutputStream();
//将输入流封装成Response对象
Response response=new Response(out);
}
- Servlet优化
这时候Tomcat已经相对于前面的优雅一些了,此时我们来假设一个业务场景,比如需要做一个登陆的功能,肯定要拿到客户端传来的用户名和密码。也就是业务代码一定要获取到用户名和密码,但是如果使用自己写的Tomcat,发现Request对象又被封装在了Tomcat内部,发现获取该对象不是太方便,所以得换个思路。其实在JavaEE的Servlet规范中,Servlet帮我们封装好了请求和响应的类,大家一定很熟悉。
class Servlet{
service(Request request,Response response){
...
}
}
也就是说如果我们想要进行业务代码的开发,比如登录,可以写一个类继承Servlet,然后就可以获取到客户端传来的请求和响应。
class LoginServlet extends Servlet{
doService(Request request,Response response){
...
}
}
这样一来就可以顺利拿到客户端传来的数据以及返回数据给客户端。
关键是这里的request和原来我们自己写的tomcat中的request的关系是如何的呢?这样不就重复定义了吗?好像有点,那不妨把类似于LoginServlet添加到Tomcat源码中?也就是但凡有一个servlet就加一个到Tomcat中进行处理,也就是Tomcat源码可以优化成这样。
class MyTomcat{
ServerSocket server=new ServerSocket(8080);
Socket socket=server.accept();
//这样地方就可以用一个List集合不断地把业务代码中servlet加载进来
list.add(servlets);
}
我们的web项目中到底有多少个servlet呢?根据大家的开发经验都知道,每个业务代码的Servlet类都可以在web.xml文件中进行配置,类似于下面这段配置。
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.gupao.web.servlet.SimpleServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
这样在Tomcat源码中只需要读取到每个项目对应的web.xml文件,然后解析其中的servlet标签,将一个个servlet实例化加载到list集合中即可。
2.4 思路过渡
有了上面手写的思路分析,此时可以总结出两点。第一,监听端口;第二,加载servlets。发现说起来还挺合情合理的,但是这些都是我们自己的主观臆断,官网源码是否也是这样的思路,这个我们需要验证,那接下来咱们就围绕这两点进行一下验证,先验证servlets再验证监听端口。
3 验证加载servlets和监听端口
3.1 加载servlets
那源码中到底哪里是加载servlet的呢?我先给大家一条搜索主线。
Context->StandardContext.loadOnStartup(Container children[])
这个方法就是加载每个web项目加载servlets的地方。很明显,下面方法上的英文注释已经表明了,而且在代码中也能找到list.add(wrapper)这个方法,和手写中的list.add(servlets)非常像,只要证明wrapper就是servlet即可。
/**
* Load and initialize all servlets marked "load on startup" in the
* web application deployment descriptor.
*
* @param children Array of wrappers for all currently defined
* servlets (including those not declared load on startup)
*/
public boolean loadOnStartup(Container children[]) {
// Collect "load on startup" servlets that need to be initialized
TreeMap<Integer, ArrayList<Wrapper>> map = new TreeMap<>();
for (int i = 0; i < children.length; i++) {
Wrapper wrapper = (Wrapper) children[i];
int loadOnStartup = wrapper.getLoadOnStartup();
if (loadOnStartup < 0)
continue;
Integer key = Integer.valueOf(loadOnStartup);
ArrayList<Wrapper> list = map.get(key);
if (list == null) {
list = new ArrayList<>();
map.put(key, list);
}
list.add(wrapper);
}
...
}
这时候大家可能会有两个疑惑,一是为什么是找StandardContext.loadOnStartup()这个方法,二是wrapper到底是不是servlet。接下来我们就围绕这两个问题探讨一下。
3.1.1 为什么是StandardContext.loadOnStartup()
我是这样想的,如果我是Tomcat源码的设计者,加载servlet一定会以web项目为单位,因为Tomcat中可能会有很多的web项目,而每个web项目中又有很多的Servlet类,所以以web项目为单位是最合适的。那Tomcat源码中什么可以代表一个web项目呢?问题就会聚焦到找代表web项目的东西。
先不急,我们可以回到tomcat产品中在conf下有一个server.xml文件,而这个文件是tomcat的核心配置文件,我找到一份server.xml文件,并且把其中的注释删掉了,如下所示。
<?xml version='1.0' encoding='utf-8'?>
<Server port="8005" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<GlobalNamingResources>
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Realm className="org.apache.catalina.realm.LockOutRealm">
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
</Engine>
</Service>
</Server>
大家可以先不用具体关系每个标签的含义,至少这个文件大家是见过的,而且我们可能还配置过一些标签属性之类的,比如我们想要把web项目放到Tomcat中供外界访问,可以将项目deploy到webapps下,这是一种方式。除此之外,在里面还可以配置这样的标签。是放到Host标签里面的,也就是可以通过这样的方式同样达到和deploy到webapps一样的效果。
<Context path="/gupao" docBase="E:/gupao"/>
这说明什么呢?是不是可以某种意义一个Context标签就可以代表一个web项目?是的,没错,可以。再看server.xml文件中的标签还有什么含义?看过一些框架源码的哥们应该知道,这些标签一般和源码中的类或接口是一一对应的,我们可以尝试在Tomcat源码中搜索这些标签名称的类,我这里就不带领大家搜索了,大家可以自己做一个验证。
另外我再给大家展示一张图,这张图是Tomat的架构图,通过这张图大家能够发现,图中的组件和server.xml文件中的标签名称一一对应,而且包含的层次也是一一对应的。当然,图中的有些组件的名称可能在默认server.xml文件中没有配置,图解中如果某个组件是多层的意味着该标签在server.xml文件可以配置多个。也就是说Tomcat架构图,server.xml中标签和源码中的类是一一对应的关系。
ok,啰嗦了这么多,回到我们想讨论的内容。我们已经能够知道Context标签可以代表web项目,也就是Tomcat源码中一定有一个Context类,而且该类代表的是web项目。
前面说加载servlets一定是以web项目为单位的,如果我想要找加载servlets在源码中的地方,一定是先找到Context类,发现它是一个接口,肯定继续找它的实现类,它有一个标准的实现类StandardContext。接着要想找加载servlets的地方,肯定是找这个类的方法,打开该类的所有方法,发现只有loadOnStartup是命名最像的。至此,Tomcat源码加载servlets的完成。
3.1.2 wrapper到底是不是servlet
接下来要探讨的问题就是在loadOnStartup中list.add(wrapper)这个到底是不是添加的servlet,换句话说就是wrapper到底是不是servlet。
先把脑袋放空,什么都不要想,要想证明wrapper是servlet,我觉得可以先看另外一个问题。也就是我们找到了Tomcat源码加载servlet的地方,但是都没有看到Tomcat源码解析每个web项目中web.xml文件servlet标签的过程,不妨把这个先找一下。
既然Context代表web项目,那web项目的配置命名应该是ContextConfig,源码中搜索一下该类,果然有,也就是读取web项目的配置文件是在这个类中发生的,关键是找哪个方法呢?
一不小心找到一个webConfig()
方法,如下所示
/**
* Scan the web.xml files that apply to the web application and merge them
* using the rules defined in the spec. For the global web.xml files,
* where there is duplicate configuration, the most specific level wins. ie
* an application's web.xml takes precedence over the host level or global
* web.xml file.
*/
protected void webConfig() {
...
}
该方法上的注释想必大家都能看到,英语应该都过了8级对吧?显然是扫描每个web项目中对应的web.xml文件。
- 找到web.xml文件的路径
要想加载解析web.xml文件肯定要先知道它的路径,大家可以按照下面这条线找到对应的路径。
org.apache.catalina.startup.ContextConfig#webConfig()->getContextWebXmlSource()->servletContext.getResourceAsStream(Constants.ApplicationWebXml)
我们可以看到一个ApplicationWebXml配置:
Constants.ApplicationWebXml = "/WEB-INF/web.xml";
- 解析web.xml文件中的标签并创建对象
继续回到webConfig()方法,找到step9有这样一段代码
// Step 9. Apply merged web.xml to Context
if (ok) {
configureContext(webXml);
}
也就是configureContext(webXml)就是解析的,那点开这个方法看一下咯。慢慢往下拉,会发现有很多web.xml文件中熟悉的标签,比如ErrorPage,FilterMap,listener,ContextResource等。继续往下看,会发现有这段代码。这里明显就是解析web.xml文件中的servlet,并且创建相应的对象,然后会将servlet包装成wrapper。ok,至此,我们可以确定的是wrapper就是对servlet的包装,也就是servlet。
for (ServletDef servlet : webxml.getServlets().values()) {
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
if (servlet.getLoadOnStartup() != null) {
wrapper.setLoadOnStartup(servlet.getLoadOnStartup().intValue());
}
if (servlet.getEnabled() != null) {
wrapper.setEnabled(servlet.getEnabled().booleanValue());
}
wrapper.setName(servlet.getServletName());
...
}
3.2 监听端口
经过上面一同胡说八道,终于解决了手写Tomcat中加载servlets以及源码验证部分的内容。接下来就是要解决监听端口在Tomcat源码中是否也有同样的实现方式。说白了就是要找到new ServerSocket(port)这样的代码。
关键是从哪入手呢?不妨把目光还是转移到server.xml文件和Tomcat架构图,忘了那张架构图的可以往上翻一下,我这里就不贴出来咯。要想和客户端发生点关系,用脚指头想也知道肯定和Connector相关,也就是说和源码中Connector类相关,那接下来咱们就从Connector开始找找ServerSocket在哪。
Connector.initInternal()->protocolHandler.init()->AbstractProtocol.init()->endpoint.init()->bind()-JIoEndpoint.bind()
对于bind()方法其实支持很多种IO实现方式,比如有BIO,NIO,NIO2等。
打开JIoEndpoint.bind()方法,发现其中有这段代码
if (getAddress() == null) {
serverSocket = serverSocketFactory.createSocket(getPort(),
getBacklog());
} else {
serverSocket = serverSocketFactory.createSocket(getPort(),
getBacklog(), getAddress());
}
很明显,这不就是我们要找的ServerSocket创建的代码吗?继续点开createSocket(getPort(),
getBacklog())方法,找到DefaultServerSocketFactory的实现。
@Override
public ServerSocket createSocket (int port, int backlog)
throws IOException {
return new ServerSocket (port, backlog);
}
ok,到这里已经找到了Tomcat源码中使用ServerSocket监听的代码部分,这其实也是BIO的实现方法,也就是JIo,即Java IO[传统Java IO的实现方式]
4 Tomcat整体源码阅读
经过上面的折腾,我们已经能从Tomcat中找到加载servlet和监听端口所在的地方,接下来我们将整个过程串一串,也就是从Tomcat启动开始大概经历的过程。
4.1 Tomcat启动入口类
既然Tomcat是Java开发的,那么肯定有一个入口类,这个类中一定会有一个main函数。
从方法的注释可以看得出来,该方法是启动Tomcat的一个入口方法。
/**
* Main method and entry point when starting Tomcat via the provided
* scripts.
*
* @param args Command line arguments to be processed
*/
public static void main(String args[]) {
if (daemon == null) {
// Don't set daemon until init() has completed
Bootstrap bootstrap = new Bootstrap();
try {
}
}
...
}
继续往下走,会发现有start
和stop
这样的字符串。顾名思义,肯定是根据脚本名称执行相应的操作,比如我们想进行start操作,这时候会调用这段代码。
if (command.equals("startd")) {
args[args.length - 1] = "start";
//先加载
daemon.load(args);
//再启动
daemon.start();
}
4.2 load()过程
当调用load的时候,发现它使用的是反射的方式调用Catalina.load()
方法。
来到该方法,发现如下代码,看上面的注释发现是Start a new server instance.也就是开始一个server的实例。
/**
* Start a new server instance.
*/
public void load() {
long t1 = System.nanoTime();
...
}
这时候我们不妨先想想,为什么是Server呢?这个名称好像在哪看过。没错,在server.xml文件中,
Tomcat架构图最外层的组件,Tomcat源码中都有这样的Server名称。是不是可以这样认为?现在Tomcat源码实际上是要根据server.xml文件来创建相应的对象,而这个创建的过程是从最外面的Server开始,然后依次往里面进行加载,比如Service,Connector等,如果是这样,我们继续往下看。
4.3 找到server.xml文件位置
不管先加载创建哪个名称,肯定是先要找到server.xml文件所在位置,来看代码
file = configFile()
->File file = new File(configFile)
->protected String configFile = "conf/server.xml";
这样就顺利找到了server.xml文件所在位置。
4.4 解析server.xml文件加载Server标签创建对象
继续回到Catalina.init()方法,该方法加载到server.xml文件之后,就会把起解析到input输入流中。
inputStream = new FileInputStream(file);
inputSource = new InputSource(file.toURI().toURL().toString());
try {
inputSource.setByteStream(inputStream);
digester.push(this);
digester.parse(inputSource);
}
然后就可以解析server.xml由外向内创建相应对象咯,这一块大家可以把之前的Tomcat架构图放在一旁结合源码一起看会更有感觉。
// Start the new server
try {
getServer().init();
} catch (LifecycleException e) {
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE")) {
throw new java.lang.Error(e);
} else {
log.error("Catalina.start", e);
}
}
4.5 LifeCycle的意义
点开getServer().init()方法,发现来到的是LifeCycle.init()方法,很奇怪,为啥?大家想一想,无论是Server,Service,Connector等这些组件是不是都有会init,start,destroy方法,也就是说它们都会有这些生命周期,所以按照Java面向对象开发的思想,不妨把这个生命周期统一管理起来,于是有了LifeCycle。通过会将LifeCycle设计为接口,然后会有默认的实现类LifeCycleBase
。
public interface Lifecycle {
// ----------------------------------------------------- Manifest Constants
/**
* The LifecycleEvent type for the "component after init" event.
*/
public static final String BEFORE_INIT_EVENT = "before_init";
}
所以LifeCycle.init()会继续调用LifeCycleBase.init()方法,在LifeCycleBase.init()中会调用initInternal(),发现该方法是抽象的,所以要找对应的实现类。具体找哪个实现类要根据目前想要初始化的类是哪一个。
protected abstract void initInternal() throws LifecycleException;
比如目前要初始化的Server对象,那肯定是调用Server的initInternal()方法。
4.6 StandardServer.initInternal()
通常Server,Service都是一个接口,所以要找它们对应的实现类,比如Server就会找StandardServer类,也就是调用StandardServer.initInternal()方法对其进行初始化,最后会有这段代码。即初始化完Server之后,要对里面的小弟进行初始化,比如Service,大家可以结合Tomcat架构图来看,只不过发现这边使用的for循环,为何?从架构图中可以看到Service是可以有多层的,所以当然要用循环来初始化。
// Initialize our defined Services
for (int i = 0; i < services.length; i++) {
services[i].init();
}
4.7 services[i].init()
继续点开service[i].init()方法,查看init的逻辑,发现还是来到的是LifeCycle.init(),意料之中,也就是会走这条调用线路:service[i].init()
-LifeCycle.init()
->LifeCycleBase.init()
->initInternal()
->StandardService.initInternal()
由下面这段代码可以发现,初始化完Service之后,会初始化Service里面的小弟,这边大家一定要结合架构图来看,比如有Executor,Connector等,而且用的也是循环结构,因为在架构图显示的是多层。比如我们来看Connector的初始化。
@Override
protected void initInternal() throws LifecycleException {
super.initInternal();
if (container != null) {
container.init();
}
// Initialize any Executors
for (Executor executor : findExecutors()) {
if (executor instanceof JmxEnabled) {
((JmxEnabled) executor).setDomain(getDomain());
}
executor.init();
}
// Initialize mapper listener
mapperListener.init();
// Initialize our defined Connectors
synchronized (connectorsLock) {
for (Connector connector : connectors) {
try {
connector.init();
} catch (Exception e) {
String message = sm.getString(
"standardService.connector.initFailed", connector);
log.error(message, e);
if (Boolean.getBoolean("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE"))
throw new LifecycleException(message);
}
}
}
}
4.8 connector.init()
connector这个组件在前面分析监听端口的时候,我们看到接触过。接下来我们来看下connector.init()调用流程。
connector.init()
->LifeCycleBase.init()
->LifeCycleBase.initInternal()
->Connector.initInternal()
这块有没有想起对于Connector监听端口我们找的就是initInternal()方法?这样就和前面监听端口的部分串联起来了,然后往下走的话就是这样的流程。
Connector.initInternal()
->protocolHandler.init()
->AbstractProtocol.init()
->endpoint.init()
->bind()
-JIoEndpoint.bind()
4.9 小结
通过上面流程的分析,我们能知道Tomcat从入口类开始的整体流程,我也给大家画好了一张总结图,大家可以对照这个图再看看前面的分析,希望对大家有所帮助。如若有不对之处,欢迎大家一起探讨交流,谢谢。