Tomcat8 source code analysis

Tomcat is a web server that everyone is very familiar with. I won’t go into details about its specific functions and how to use it. Today we mainly analyze the source code of Tomcat, which is based on version 8.0.11. The question is where to start? I think so, it must first download the source code of Tomcat8.0.11 from the official website, and then import it into the project.

1 Tomcat source code download and import project

1.1 Source code download

Tomcat version: Tomcat8.0.11

Download address: Click to download

Then select the corresponding platform

1.2 pom file preparation and import project

After downloading, unzip it. I am based on the windows platform. We can build it based on maven, so we need to introduce the dependencies related to the pom.xml file. The following is the pom.xml file I prepared for you. You can use it directly. Put it in the root directory of the source code.

<?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>

After that, you can import the source code into the project. Here I am using idea. After the import is completed, it is shown in the figure below.

The next step is to analyze the source code? Don't worry, we don't have any ideas in the analysis now, and we are completely confused, so we have to think about how to start. After looking at these files, we can conclude that Tomcat is developed in Java language. In fact, everyone may know this from the beginning, and it is nothing special. Since it is developed in the Java language, and we are Java developers, we might as well write a Tomcat by hand first, and then deduce the implementation of the source code based on the handwritten ideas. Isn’t this seamless integration? ok, then write it by hand.

2 Handwritten Tomcat

Before handwriting, think about the role of Tomcat? It is a web server, which provides access to the client and returns resources to the client. At this time, it is necessary to write a piece of code to allow the client to connect and then perform data interaction. How to let the client connect up? Socket programming can be adopted with existing knowledge in Java.

2.1 ServerSocket listening port waiting for client connection

class MyTomcat{
    ServerSocket server=new ServerSocket(8080);
    Socket socket=server.accept();
}

2.2 Process data interaction according to the connection of the client

class MyTomcat{
    ServerSocket server=new ServerSocket(8080);
    Socket socket=server.accept();
    //接受客户端传来的数据
    InputStream in=socket.getInputstream();
    //进行相应的业务处理
    ......
    //返回给客户端数据
    OutputStream out=socket.getOutputStream();
}

So far, a mini version of Tomcat has been completed through Socket programming and IO. The key is that we have to think about whether it can be optimized.

2.3 Optimizing Tomcat

  • Request and Response optimization

了解过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 &quot;%r&quot; %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 {
        }
    }
    ...
}

继续往下走,会发现有startstop这样的字符串。顾名思义,肯定是根据脚本名称执行相应的操作,比如我们想进行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从入口类开始的整体流程,我也给大家画好了一张总结图,大家可以对照这个图再看看前面的分析,希望对大家有所帮助。如若有不对之处,欢迎大家一起探讨交流,谢谢。

Guess you like

Origin blog.csdn.net/m0_53121042/article/details/116690670