当前位置: > Linux服务器 > Tomcat >

Tomcat源代码分析

时间:2016-05-29 22:04来源:linux.it.net.cn 作者:IT

我下载的源码版本是7.0.50,你也可以从官网下载最新版的源代码,因为我们线上使用的是这个版,因此研究的也是这个,7.0版本的总体上变化应该不大,对研究学习里面的主干内容没有什么影响。

根据官方的文档,需要使用ant这个比较古老的编译工具,实在有点繁琐,网上搜了一下,imtiger给出了比较好的解决方案,可以自己增加pom文件,然后生成Eclipse工程,非常方便。

导入Eclipse

首先将下载的apache-tomcat-7.0.50-src.tar.gz解压到tomcat目录中,然后在tomcat目录中创建一个pom.xml文件,内容如下:

<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/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.imtiger</groupId> <artifactId>tomcat-study</artifactId> <name>Tomcat 7.0 Study</name> <version>1.0</version> <packaging>pom</packaging> <modules> <module>tomcat-7.0.50-src</module> </modules> </project>

然后在tomcat-7.0.50-src目录下创建一个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>Tomcat7.0</artifactId> <name>Tomcat7.0</name> <version>7.0</version> <build> <finalName>Tomcat7.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.6</source> <target>1.6</target> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.4</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.eclipse.jdt.core.compiler</groupId> <artifactId>ecj</artifactId> <version>4.2.2</version> </dependency> </dependencies> </project>

最后,在tomcat目录下执行mvn eclipse:eclipse生成Eclipse工程,导入进去即可,效果如下:

 

在搭建Web应用时,一般都会用Tomcat或者Jboss或Jetty这样的容器,我们一般是不关心启动入口的,它被隐藏在非常深的地方,但研究Tomcat的源代码,首要的任务就是找到启动入口,这样才能一步步调试并顺藤摸瓜理清整个脉络。因为已经从别的地方看到,Tomcat启动的入口类是org.apache.catalina.startup.Bootstrap,入口方法是main(),所以就省去了寻找的过程,如果在研究其它开源代码不知道的话,该怎么办呢,一个个去查看显然不太现实,像Tomcat的java源文件有1792个,那得找到猴年马月呀。但借助shell命令,查找包括main方法的类,再根据文件名猜测入口类,就变得简单多了,Cassandra的源代码我就是使用这种方式。

find .|grep '\.java$'|grep -v test|grep -v examples|grep -v modules|xargs grep 'void main('

结果如下,把包含util关键词的排除掉,就很容易找到入口类了:

./java/org/apache/catalina/ha/deploy/FileMessageFactory.java:    public static void main(String[] args) throws Exception {
./java/org/apache/catalina/realm/RealmBase.java:    public static void main(String args[]) {
./java/org/apache/catalina/startup/Bootstrap.java:    public static void main(String args[]) {
./java/org/apache/catalina/startup/Tool.java:    public static void main(String args[]) {
./java/org/apache/catalina/tribes/membership/Constants.java:    public static void main(String[] args) throws Exception {
./java/org/apache/catalina/tribes/membership/McastService.java:    public static void main(String args[]) throws Exception {
./java/org/apache/catalina/util/ServerInfo.java:    public static void main(String args[]) {
./java/org/apache/jasper/compiler/SmapGenerator.java:    public static void main(String args[]) {
./java/org/apache/jasper/JspC.java:    public static void main(String arg[]) {
./java/org/apache/tomcat/util/bcel/classfile/Method.java:     * `public static void main(String[] args) throws IOException', e.g.
./java/org/apache/tomcat/util/bcel/classfile/Utility.java:     * `void main(String[])' and throws a `ClassFormatException' when the parsed

启动的总体流程

1)进入到BootStrap#main()方法中

  2)创建一个Bootstrap实例,并调用它的init()方法进行初始化,包括setCatalinaHome()、setCatalinaBase()、initClassLoaders(),并创建一个Catalina类的实例catalinaDaemon。

  3)然后将该实例赋给Boostrap的实例daemon,并调用load(args)方法进行加载,它实际上会使用反射的方法,调用上面的catalinaDaemon实例的load()方法初始化Server、Service、Connector等
    - 在load()方法中调用createStartDigester()创建Digester实例,设置StandardServer作为默认的server,设置StandardThreadExecutor作为默认的Executor等信息
    - 在load()方法中加载conf/server.xml配置文件
    - 调用StandardServer#init()方法初始化
      - 调用initInernal()方法,注册MBean并初始化注册的所有服务,这里是在Ditester中定义的StandardServce#init()
        - 然后调用StandardService#initInternal()方法
          - 执行container.init()初始化StandardEngine类的容器实例
          - 执行executor.init()初始化所有Executor
          - 执行connector.init()初始化所有Connector,比如Ajp、Bio或Nio Connector
            - 调用initInternal()方法
              - 初始化协议处理器,比如Http11NioProtocal
              - 调用protocalHandler.init()初始化协议处理器(实际调用父类AbstractProtocal的init()方法)
                - 注册Mbean
                - 设置Endpoint的名称,比如http-nio-8080
                - 调用endpoint.init()方法初始化
                  - 调用父类AbstractEndpoint#init()方法
                    - 调用bind()方法,绑定地址和端口
              - 调用mapperListener.init()方法,初始化listener(只是注册Mbean)

  4)使用反射的方法,调用catalinaDaemon.start()启动Server
     - 调用startInternal()方法,启动所有service,这里默认是StandardService#start()
     - 调用LifecycleBase#start()方法
       - 调用startInteral()方法 
         - 调用(StandardEngine)container.start()启动Engine
           - 调用StandardEngine#startInternal()
             - 调用父类ContainerBase#startInternal()
               - 执行findChildren()方法获取子容器,比如StandardHost的实例
               - 将这些子容器用StartChild类包装成Callable的类,使用线程池启动
                 - 调用StartChild#call()方法
                   - 调用StandardHost#start()方法启动Host
                     - 调用StandardHost#startInternal()
                       - 调用getPipeline().addValve()增加ErrorReportValve
                       - 调用super.startInternal()使用父类ContainerBase的启动方法部署应用
                         - 调用setState() 方法设置Context,这个方法有有点误导人,它里面其实做了很多工作,不仅仅是设置个状态值
                           - 调用LifecyleBase#setStateInternal()
                             - 调用fireLifeCycleEvent(“start”,null)
                               - 调用LifecycleSurpport#fireLifecycleEvent()
                                 - 获取注册过的侦听器,这里是HostConfig
                                   - 调用HostConfig#lifecycleEvent()
                                     - 调用start()方法
                                       - 调用deployApps()部署应用
                                         - 调用deployWARs()部署war包
                                         - 调用deployDirectories()部署解压后的目录
                                           - 获取webapps目录下的所有目录,比如docs, examples, host-manager, manager, ROOT
                                        - 将之用实现Callable接口的DeployDirectory包装,放入到线程池中启动
                                          - 调用DeployDirectory#run()方法启动线程
                                            - 调用HostConfig#deployDirectories(ContextName,File)
                                              - 调用digester.parse(“webapps/docs/META-INF/context.xml")创建一个StandardContext实例
                                              - 调用host.addChild(context)将之增加到StandardHost实例中
                                                - 调用ContainerBase#addChildInternal()
                                                  - 调用child.start()启动StandardContext
         - 调用executor.start()启动Executor
         - 调用connector.start()启动Connector
           - 调用startInternal()
             - 调用protocalHandler.start()启动处理器
               - 调用父类AbstractProtocal#start()
                 - 调用Endpoint的子类,比如JIoEndpoint的start()方法
                   - 调用createExecutor()方法创建Executor
                     - 创建TaskQueue
                     - 创建一个ThreadPoolExecutor
                   - 调用startAcceptorThreads()方法启动接收器线程
                     - 调用getAcceptorThreadCount()获取要启动的接收器线程个数
                     - 调用createAcceptor()创建接收器,比如NioEndpoint.Acceptor
                     - 将它放到线程中启动(实际调用具体的NioEndpoint.Acceptor.run())
                       - 调用countUpOrAwaitConnection()检查最大连接数,如果超过就等待
                       - 调用(ServerSocketChannel)serverSock.accept()等待下一个连接进来,此时会阻塞到这里
             - 调用mapperListner.start()启动listener

  5) 最后在控制台打印启动时间,完成Tomcat的全部启动过程

关闭的整体流程

1)调用(Catalina)catalinaDaemon.start()启动Tomcat之后,在方法的最后将运行到await()方法这里,并阻塞
    - 调用 getServer().await()方法,实际上是StandardServer#await()
      - 创建一个ServerSocket,侦听默认的8005端口
      - 调用serverSocket.accept()方法阻塞,等待连接的进入
      - 有连接进来后,读取请求命令是否等于SHUTDOWN,如果是的话就关闭Socket连接
2)调用(Catalina)catalinaDaemon.stop()方法,停止Tomcat
      - 获取StandardServer实例
      - 调用它的stop()方法,实际调用stopInternal()
        - 调用service.stop()停止所有服务
          - 调用StandardService.stopInternal()方法
            - 调用Connector#pause()
              - 调用(Http11NioProtocal)protocalHandler.pause(),这里会调用父类AbstractProtocal的pause()方法
                - 调用(NioEndpoint)endpoint.pause()
                  - 调用unlockAccept()
                    - 在这里结束掉Acceptor,停止接收新的连接请求
      - 调用它的destroy()方法
        - 调用StandardServer#destoryInternal()
          - 调用StandandService#destroy()方法
          - 注销MBean

 

在conf/server.xml文件中默认是使用BIO模式的,但这种阻塞式的方式效率不高,线上一般会使用NIO的方式。可以在connector中配置protocal="org.apache.coyote.http11.Http11NioProtocol"来指定,Tomcat启动后就会使用NioEndpoint.Acceptor.run()来接收外部请求的Socket连接,然后把它注册到一个队列中:

- 调用countUpOrWaitConnection(),如果连接超过最大连接数,就等待
  - 调用serverSocket.accept()创建一个新的SocketChannel实例,这个地方会阻塞,有连接进来后会往下执行
  - 调用setSocketOptions(SocketChannel socket)
    - 调用 getPoller0().register(channel) 将channel添加到Poller中
      - 调用Poller#regiester()方法
        - 调用addEvent(PollerEvent r) 
          - 调用(ConcurrentLinkedQueue<Runnable>)events.offer(r) 把它加入队列中

Poller会在执行NioEndpoint#startInternal()中创建,启动单独的线程,检测队列中是否有PollerEvent事件,如果有就进行请求的处理:

- 运行run()
    - 调用events()
      - 启动PollerEvent线程,将(NioChannel)socket.getPoller().getSelector()注册到SocketChannel中
    - 调用selector.selectedKeys().iterator()得到SelectionKey集合
    - 遍历这个集合,执行processKey()方法
      - 调用processSocket()方法处理连接请求
        - 调用getExecutor().execute(SocketProcessor sc) 启动一个处理线程处理请求

获取一个Executor之后,就开始真正的业务逻辑处理的,我们在业务代码中打印出来的调用栈就是这个线程的调用栈,上面的执行过程如果不看源代码是很难知道Tomcat是如何处理的,它的调用栈如下:

cn.fraudmetrix.forseti.api.service.RiskServiceImpl.execute(RiskServiceImpl.java)
sun.reflect.GeneratedMethodAccessor317.invoke(Unknown Source)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96)
org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260)
org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
com.sun.proxy.$Proxy238.execute(Unknown Source)
sun.reflect.GeneratedMethodAccessor317.invoke(Unknown Source)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:317)
org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:183)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:150)
org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:96)
org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:260)
org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94)
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)
org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
com.sun.proxy.$Proxy239.execute(Unknown Source)
cn.fraudmetrix.forseti.api.module.screen.RiskService.execute(RiskService.java:60)
cn.fraudmetrix.forseti.api.module.screen.RiskService$$FastClassByCGLIB$$7888f58c.invoke(<generated>)
net.sf.cglib.reflect.FastMethod.invoke(FastMethod.java:53)
com.alibaba.citrus.service.moduleloader.impl.adapter.MethodInvoker.invoke(MethodInvoker.java:70)
com.alibaba.citrus.service.moduleloader.impl.adapter.DataBindingAdapter.executeAndReturn(DataBindingAdapter.java:41)
com.alibaba.citrus.turbine.pipeline.valve.PerformScreenValve.performScreenModule(PerformScreenValve.java:111)
com.alibaba.citrus.turbine.pipeline.valve.PerformScreenValve.invoke(PerformScreenValve.java:74)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invoke(PipelineImpl.java:210)
com.alibaba.citrus.service.pipeline.impl.valve.ChooseValve.invoke(ChooseValve.java:98)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invoke(PipelineImpl.java:210)
com.alibaba.citrus.service.pipeline.impl.valve.LoopValve.invokeBody(LoopValve.java:105)
com.alibaba.citrus.service.pipeline.impl.valve.LoopValve.invoke(LoopValve.java:83)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
cn.fraudmetrix.forseti.api.valve.PrivilegeValidateValve.invoke(PrivilegeValidateValve.java:72)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.turbine.pipeline.valve.AnalyzeURLValve.invoke(AnalyzeURLValve.java:126)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.turbine.pipeline.valve.SetLoggingContextValve.invoke(SetLoggingContextValve.java:66)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.turbine.pipeline.valve.PrepareForTurbineValve.invoke(PrepareForTurbineValve.java:52)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invoke(PipelineImpl.java:210)
com.alibaba.citrus.service.pipeline.impl.valve.TryCatchFinallyValve.invoke(TryCatchFinallyValve.java:83)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invokeNext(PipelineImpl.java:157)
com.alibaba.citrus.service.pipeline.impl.PipelineImpl$PipelineContextImpl.invoke(PipelineImpl.java:210)
com.alibaba.citrus.webx.impl.WebxControllerImpl.service(WebxControllerImpl.java:43)
com.alibaba.citrus.webx.impl.WebxRootControllerImpl.handleRequest(WebxRootControllerImpl.java:53)
com.alibaba.citrus.webx.support.AbstractWebxRootController.service(AbstractWebxRootController.java:165)
com.alibaba.citrus.webx.servlet.WebxFrameworkFilter.doFilter(WebxFrameworkFilter.java:152)
com.alibaba.citrus.webx.servlet.FilterBean.doFilter(FilterBean.java:148)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
com.alibaba.citrus.webx.servlet.SetLoggingContextFilter.doFilter(SetLoggingContextFilter.java:61)
com.alibaba.citrus.webx.servlet.FilterBean.doFilter(FilterBean.java:148)
org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:222)
org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:123)
org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:502)
org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:171)
org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)
org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:953)
org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)
org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:409)
org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1044)
org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1721)
org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.run(NioEndpoint.java:1679)
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
java.lang.Thread.run(Thread.java:744)

在getExecutor().execute(SocketProcessor sc)是有个坑的,conf/server.xml中配置Executor的参数时通常有如下几个:

<Executor 
    name="tomcatThreadPool" 
    namePrefix="catalina-exec-"
    maxThreads="150" 
    minSpareThreads="4"
    maxIdleTime="60000"
    maxQueueSize="100"/>

最关键的后面四个参数,maxThreads是线程池的最大活动线程数,可以把它理解为线程池最多能同时开启的线程数。minSpareThreads是保留的最小活动闲线程数,也是说不管有没有请求,活动线程数最少是4。maxIdleTime是指超过4的线程最大空闲时间,超过这个时间线程还没有使用将会被关闭。maxQueueSize是指当已在有4个线程在运行中了,如果再有请求进来会将它放到队列中,最多能放100,默认是Integer.MAX_VALUE,基本上赞同于无界队列了。最开始没有完全搞清楚minSpareThreads的含义,从官方文档上看以为是保留的最小活动线程数,如果有请求超过这个数目,还会创建新的线程来执行,只要不超过最大值150就行,如果超过最大值就会放入队列,超过队列的长度就会被拒绝。但看源代码后才发现不是这么回事:

public abstract class AbstractEndpoint { ... public void createExecutor() { internalExecutor = true; TaskQueue taskqueue = new TaskQueue(); TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority()); executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf); taskqueue.setParent( (ThreadPoolExecutor) executor); } ... } public class ThreadPoolExecutor extends java.util.concurrent.ThreadPoolExecutor { ... public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) { super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, new RejectHandler()); } ... }

从上面的代码来看,Exector最终还是用了Jdk的ThreadPoolExecutor,minSpareThreads实际上指的就是corePoolSize,对它了解的人都知道,将请求数小于等于它时会马上创建一个新的线程,如果大于它就会把新的请求放到队列中,如果请求数超过corePoolSize加上队列的长度但又小于maxPoolSize时也会马上创建新的线程。而Tomcat中默认是使用了一个无界队列TaskQueue,它继承自LinkedBlockingQueue<Runnable>,在上面设置maxThreads和maxQueueSize其实是没有意义的,因为默认创建的是无界队列根本没有读取配置的参数几乎永远不会满,自然不存在超过需要用maxThreads来判断了。

像我们的业务场景对执行时间要求非常高,如果线程无法马上创建而被放入队列等待前面的请求释放资源就以为着可能会超时,再执行已毫无意义,因此需要将minSpareThreads值设置的大一点,一有请求就马上创建线程,不要等待下去。

通过阅读源代码可以更好的理解系统的执行原理,便于参数的合理设置和快速定位问题。





(责任编辑:IT)
------分隔线----------------------------
栏目列表
推荐内容