3_tomcat初始化过程

启动类BootStrap

BootStrap是Tomcat的入口。Main方法和static语句块 

        设置catalinaHome(安装目录)和catalinaBase(工作目录)两个路径

static {
        // Will always be non-null
        String userDir = System.getProperty("user.dir");

        // Home first
        String home = System.getProperty(Globals.CATALINA_HOME_PROP);
        File homeFile = null;
        //获取tomcat 的安装目录
        if (home != null) {
            File f = new File(home);
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

       省略代码......

        
        //设置catalinaHomeFile
        catalinaHomeFile = homeFile;
        System.setProperty(
                Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());

        // Then base
        String base = System.getProperty(Globals.CATALINA_BASE_PROP);
        //设置Tomcat工作目录
        if (base == null) {
            catalinaBaseFile = catalinaHomeFile;
        } else {
            File baseFile = new File(base);
            try {
                baseFile = baseFile.getCanonicalFile();
            } catch (IOException ioe) {
                baseFile = baseFile.getAbsoluteFile();
            }
            catalinaBaseFile = baseFile;
        }
        System.setProperty(
                Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());
    }

mian方法的作用:

1 实例化BootStrap
2 初始化BootStrap
3 daemon.load(args)
4 daemon.start()

先说初始化,初始化BootStrap过程:

public void init() throws Exception {

    // 初始化commonLoader、catalinaLoader、sharedLoader,关于ClassLoader的后面再单独分析
    initClassLoaders();

    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);

    // 反射方法实例化Catalina,后面初始化Catalina也用了很多反射,不知道意图是什么
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();

    // 反射调用setParentClassLoader方法,设置其parentClassLoader为sharedLoader
    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);

    // 引用Catalina实例
    catalinaDaemon = startupInstance;
}

 initClassLoaders 涉及到打破双亲委派机制,注释代码

ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null;
    // -------------------------------------------------------- Private Methods
    /**Tomcat三个类加载器,Tomcat自定义的 URLClassLoader
     *JVM也有类加载器
     *SystemClassLoader是这个的父类加载器
     * Tomcat为了打破双亲委派机制,提高效率,先从当前的sharedLoaderce加载,当前加载器没有
     * 再到父类加载器commonLoader进行加载,
     * sharedLoader 所有web 程序的类加载器,commonLoader是 sharedLoaderce 的父类加载器
     * sharedLoader加载webapps里加载WEB-INFO 中的class 和lib
     */
    private void initClassLoaders() {
        try {
            // commonLoader的父类加载器置为空,打破了双亲委派机制,提高效率
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                // no config file, default to this loader - we might be in a 'single' env.
                commonLoader = this.getClass().getClassLoader();
            }
            catalinaLoader = createClassLoader("server", commonLoader);
            sharedLoader = createClassLoader("shared", commonLoader);
        } catch (Throwable t) {
            handleThrowable(t);
            log.error("Class loader creation threw exception", t);
            System.exit(1);
        }
    }

load & start

初始化Bootstrap之后,接下来就是加载配置,启动容器。而load、start实际上是由Bootstrap反射调用Catalina的load、start,这一部分代码将在下面的Catalina部分进行分析

  • 启动时,Catalina.setAwait(true),其目的是为了让tomcat在关闭端口阻塞监听关闭命令,参考Catalina.await()方法
  • deamon.load(args),实际上会去调用Catalina#load(args)方法,会去初始化一些资源,优先加载conf/server.xml,找不到再去加载server-embed.xml;此外,load方法还会初始化Server
  • daemon.start(),实例上是调用Catalina.start()

Catalina初始化

由前面的分析,可知Bootstrap中的load逻辑实际上是交给Catalina去处理的,下面我们对Catalina的初始化过程进行分析

load(init)

load阶段主要是通过读取conf/server.xml或者server-embed.xml,实例化Server、Service、Connector、Engine、Host等组件,并调用Lifecycle#init()完成初始化动作,以及发出INITIALIZING、INITIALIZED事件

  • 首先初始化jmx的环境变量
  • 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类,如果我们要改变server.xml的某个属性值(比如优化tomcat线程池),直接查看对应实现类的setXXX方法即可
  • 解析conf/server.xml或者server-embed.xml,并且实例化对应的组件并且赋值操作,比如Server、Container、Connector等等
  • 为Server设置catalina信息,指定Catalina实例,设置catalina的home、base路径
  • 调用StarndServer#init()方法,完成各个组件的初始化,并且由parent组件初始化child组件,一层套一层
public void load() {
    initDirs();
    // 初始化jmx的环境变量
    initNaming();
    // Create and execute our Digester
    // 定义解析server.xml的配置,告诉Digester哪个xml标签应该解析成什么类
    Digester digester = createStartDigester();
    InputSource inputSource = null;
    InputStream inputStream = null;
    File file = null;
    try {
      // 首先尝试加载conf/server.xml,省略部分代码......
      // 如果不存在conf/server.xml,则加载server-embed.xml(该xml在catalina.jar中),省略部分代码......
      // 如果还是加载不到xml,则直接return,省略部分代码......
      try {
          inputSource.setByteStream(inputStream);
          // 把Catalina作为一个顶级实例
          digester.push(this);
          // 解析过程会实例化各个组件,比如Server、Container、Connector等
          digester.parse(inputSource);
      } catch (SAXParseException spe) {
          // 处理异常......
      }
    } finally {
        // 关闭IO流......
    }
    // 给Server设置catalina信息
    getServer().setCatalina(this);
    getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
    getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
    // Stream redirection
    initStreams();
    // 调用Lifecycle的init阶段
     初始化 server 
    try {
        getServer().init();
    } catch (LifecycleException e) {
        // ......
    }
    // ......
}

StanderServer 初始化

StandardServer是由Catalina进行init初始化的,调用的是LifecycleBase父类的init方法,而StandardServer继承至LifecycleMBeanBase,重写了initInternal方法

protected void initInternal() throws LifecycleException {
    super.initInternal();
    // 往jmx中注册全局的String cache,尽管这个cache是全局听,但是如果在同一个jvm中存在多个Server,
    // 那么则会注册多个不同名字的StringCache,这种情况在内嵌的tomcat中可能会出现
    onameStringCache = register(new StringCache(), "type=StringCache");

    // 注册MBeanFactory,用来管理Server
    MBeanFactory factory = new MBeanFactory();
    factory.setContainer(this);
    onameMBeanFactory = register(factory, "type=MBeanFactory");

    // 往jmx中注册全局的NamingResources
    globalNamingResources.init();

    // Populate the extension validator with JARs from common and shared class loaders
    if (getCatalina() != null) {
        // 忽略ClassLoader操作
    }

    // 初始化内部的Service
    for (int i = 0; i < services.length; i++) {
        services[i].init();
    }
}
  • 先是调用super.initInternal(),把自己注册到jmx
  • 然后注册StringCache和MBeanFactory
  • 初始化NamingResources,就是server.xml中指定的GlobalNamingResources
  • 调用Service子容器的init方法,让Service组件完成初始化,注意:在同一个Server下面,可能存在多个Service组件

Service初始化

StandardService和StandardServer都是继承至LifecycleMBeanBase,因此公共的初始化逻辑都是一样的

protected void initInternal() throws LifecycleException {

    // 往jmx中注册自己
    super.initInternal();

    // 初始化Engine
    if (engine != null) {
        engine.init();
    }

    // 存在Executor线程池,则进行初始化,默认是没有的
    for (Executor executor : findExecutors()) {
        if (executor instanceof JmxEnabled) {
            ((JmxEnabled) executor).setDomain(getDomain());
        }
        executor.init();
    }

    // 暂时不知道这个MapperListener的作用,可能是Mapper映射器的监听器
    mapperListener.init();

   // 初始化Connector,而Connector又会对ProtocolHandler进行初始化,开启应用端口的监听
    synchronized (connectorsLock) {
        for (Connector connector : connectors) {
            try {
                connector.init();
            } catch (Exception e) {
                // 省略部分代码,logger and throw exception
            }
        }
    }
}
  • 首先,往jmx中注册StandardService
  • 初始化Engine,而Engine初始化过程中会去初始化Realm(权限相关的组件)
  • 如果存在Executor线程池,还会进行init操作,这个Excecutor是tomcat的接口,继承至java.util.concurrent.Executor、org.apache.catalina.Lifecycle
  • 初始化Connector连接器,默认有http1.1、ajp连接器,而这个Connector初始化过程,又会对ProtocolHandler进行初始化,开启应用端口的监听

Engine初始化

StandardEngine在init阶段,需要获取Realm,这个Realm是干嘛用的?

Realm(域)是用于对单个用户进行身份验证的底层安全领域的只读外观,并标识与这些用户相关联的安全角色。

protected void initInternal() throws LifecycleException {
    getRealm();
    super.initInternal();//调用父类的方法
}

public Realm getRealm() {
    Realm configured = super.getRealm();
    if (configured == null) {
        configured = new NullRealm();
        this.setRealm(configured);
    }
    return configured;
}

StandardEngine继承至ContainerBase,而ContainerBase重写了initInternal()方法,用于初始化start、stop线程池,

// 默认是1个线程

private int startStopThreads = 1;
protected ThreadPoolExecutor startStopExecutor;

@Override
protected void initInternal() throws LifecycleException {
    BlockingQueue<Runnable> startStopQueue = new LinkedBlockingQueue<>();
    
    //创建启停连接池 Engine的启停交给线程池处理
    startStopExecutor = new ThreadPoolExecutor(
            getStartStopThreadsInternal(),
            getStartStopThreadsInternal(), 10, TimeUnit.SECONDS,
            startStopQueue,
            new StartStopThreadFactory(getName() + "-startStop-"));
    // 允许core线程超时未获取任务时退出
    startStopExecutor.allowCoreThreadTimeOut(true);
    super.initInternal();
}

private int getStartStopThreadsInternal() {
    int result = getStartStopThreads();

    if (result > 0) {
        return result;
    }
    result = Runtime.getRuntime().availableProcessors() + result;
    if (result < 1) {
        result = 1;
    }
    return result;
}

这个startStopExecutor线程池有什么用呢?

  • 在start的时候,如果发现有子容器,则会把子容器的start操作放在线程池中进行处理
  • 在stop的时候,也会把stop操作放在线程池中处理

在前面的文章中我们介绍了Container组件,StandardEngine作为顶层容器,它的直接子容器是StardandHost,但是对StandardEngine的代码分析,我们并没有发现它会对子容器StardandHost进行初始化操作,StandardEngine不按照套路出牌,而是把初始化过程放在start阶段。个人认为Host、Context、Wrapper这些容器和具体的webapp应用相关联了,初始化过程会更加耗时,因此在start阶段用多线程完成初始化以及start生命周期,否则,像顶层的Server、Service等组件需要等待Host、Context、Wrapper完成初始化才能结束初始化流程,整个初始化过程是具有传递性的。

Connector初始化

Connector也是继承至LifecycleMBeanBase,公共的初始化逻辑都是一样的。

先来看下Connector的默认配置,大部分属性配置都可以在Connector类中找到,tomcat默认开启了HTTP/1.1、AJP/1.3,其实AJP的用处不大,可以去掉。

<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"  redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

Connector定义了很多属性,比如port、redirectPort、maxCookieCount、maxPostSize等等,比较有意思的是竟然找不到connectionTimeout的定义,全文搜索后发现使用了属性名映射,估计是为了兼容以前的版本(org.apache.catalina.connector.Connector) 

public class Connector extends LifecycleMBeanBase  {
    protected void initInternal() throws LifecycleException {
    // 注册jmx
    super.initInternal();
    // 初始化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的
    adapter = new CoyoteAdapter(this);
    // protocolHandler需要指定Adapter用于处理请求
    protocolHandler.setAdapter(adapter);
    // Make sure parseBodyMethodsSet has a default
    if (null == parseBodyMethodsSet) {
        setParseBodyMethods(getParseBodyMethods());
    }
    // apr支持,忽略部分代码......
    // 初始化ProtocolHandler,这个init不是Lifecycle定义的init,而是ProtocolHandler接口的init
    try {
        protocolHandler.init();
    } catch (Exception e) {
        throw new LifecycleException(
                sm.getString("coyoteConnector.protocolHandlerInitializationFailed"), e);
    }
}

initInternal过程如下所示:

  • 实例化Coyote适配器,这个适配器是用于Coyote的Request、Response与HttpServlet的Request、Response适配的,后续的博客会进行深入分析
  • 为ProtocolHander指定CoyoteAdapter用于处理请求
  • 初始化ProtocolHander,

ProtocolHandler初始化

ProtocolHandler,它是一个抽象的协议实现,它不同于JNI这样的Jk协议,它是单线程、基于流的协议。ProtocolHandler是一个Cycote连接器实现的主要接口,而Adapter适配器是由一个Coyote Servlet容器实现的主要接口,定义了处理请求的抽象接口。

ProtocolHandler的子类如下所示,AbstractProtocol(org.apache.coyote)是基本的实现,而NIO默认使用的是Http11NioProtocol

调用ProtocolHandler的init进行初始化是调用的AbstractProtocol,首先完成jmx的注册,然后对NioEndpoint进行初始化.

public abstract class AbstractProtocol<S> implements ProtocolHandler,
        MBeanRegistration {
    public void init() throws Exception {
        // 完成jmx注册
        if (oname == null) {
            oname = createObjectName();
            if (oname != null) {
                Registry.getRegistry(null, null).registerComponent(this, oname, null);
            }
        }
        if (this.domain != null) {
            rgOname = new ObjectName(domain + ":type=GlobalRequestProcessor,name=" + getName());
            Registry.getRegistry(null, null).registerComponent(
                    getHandler().getGlobal(), rgOname, null);
        }

        String endpointName = getName();
        endpoint.setName(endpointName.substring(1, endpointName.length()-1));
        endpoint.setDomain(domain);

        // 初始化endpoint
        endpoint.init();
    }
}

NioEndpoint初始化过程,最重要的是完成端口和地址的绑定监听工作(org.apache.tomcat.util.net.NioEndpoint)

public class NioEndpoint extends AbstractJsseEndpoint<NioChannel> {
    public void bind() throws Exception {
    // 实例化ServerSocketChannel,并且绑定端口和地址
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = (getAddress()!=null?new InetSocketAddress(getAddress(),getPort()):new InetSocketAddress(getPort()));
        // 设置最大连接数,原来是在这里设置的
        serverSock.socket().bind(addr,getAcceptCount());
        serverSock.configureBlocking(true); //mimic APR behavior
        // 初始化acceptor、poller线程的数量
        // Initialize thread count defaults for acceptor, poller
        if (acceptorThreadCount == 0) {
            // FIXME: Doesn't seem to work that well with multiple accept threads
            acceptorThreadCount = 1;
        }
        if (pollerThreadCount <= 0) {
            pollerThreadCount = 1;
        }
        setStopLatch(new CountDownLatch(pollerThreadCount));
        // 如果有必要的话初始化ssl
        initialiseSsl();
        // 初始化selector
        selectorPool.open();
    }
}

至此,整个初始化过程便告一段落。整个初始化过程,由parent组件控制child组件的初始化,一层层往下传递,直到最后全部初始化OK。

默认情况下,Server只有一个Service组件,Service组件先后对Engine、Connector进行初始化。而Engine组件并不会在初始化阶段对子容器进行初始化,Host、Context、Wrapper容器的初始化是在start阶段完成的。tomcat默认会启用HTTP1.1和AJP的Connector连接器,这两种协议默认使用Http11NioProtocol、AJPNioProtocol进行处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值