Tomcat源码笔记(二)Bootstrap启动入口main

58 篇文章 0 订阅
8 篇文章 1 订阅

本文呢,主要从main启动入口了解Tomcat的启动大致步骤和如何终止,至于更深入的源码跟踪,下篇

本文阅读到的源代码分支

目录

Bootstrap静态块

Bootstrap.main启动流程

Bootstrap.init

初始化加载器 initClassLoaders

 为什么需要三种类加载器?

 Bootstrap.load

为什么Bootstrap类要反射调用Catalina

 Bootstrap.start

注册钩子CatalinaShutdownHook

await监听shutdown端口

Bootstrap的stop流程



Bootstrap为启动入口,负责创建Catalina,根据参数调用Catalina相关方方法完成对服务器的操作
Catalina用于解析service.xml创建各个组件。同时负责启动、停止服务器Server

Bootstrap静态块

有静态块先看静态块,内部设置了CATALINA_HOME路径

public final class Bootstrap {  
   public static final String CATALINA_HOME_PROP = "catalina.home";
   public static final String CATALINA_BASE_PROP = "catalina.base";

   static {
        // Will always be non-null
        // 当前工作路径
        String userDir = System.getProperty("user.dir");

        // 开始找Catalina.home,先看是否启动设置了
        String home = System.getProperty(Globals.CATALINA_HOME_PROP);
        File homeFile = null;

        if (home != null) {
            File f = new File(home);
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

        // 启动没设置,再看当前目录下是否有bootstrap.jar
        // 如果有,表示当前是在/tomcat安装目录/bin下,上层目录就是catalina.home
        // 可以看看平时部署的tomcat目录结构,bootstrap.jar在/安装目录/bin下,startup.bat中也是类似逻辑
        if (homeFile == null) {
            // First fall-back. See if current directory is a bin directory
            // in a normal Tomcat install
            File bootstrapJar = new File(userDir, "bootstrap.jar");

            if (bootstrapJar.exists()) {
                File f = new File(userDir, "..");
                try {
                    homeFile = f.getCanonicalFile();
                } catch (IOException ioe) {
                    homeFile = f.getAbsoluteFile();
                }
            }
        }

        // 如果还没有找到,设置当前工作目录为catalina.home
        if (homeFile == null) {
            // Second fall-back. Use current directory
            File f = new File(userDir);
            try {
                homeFile = f.getCanonicalFile();
            } catch (IOException ioe) {
                homeFile = f.getAbsoluteFile();
            }
        }

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

        // catalina.base默认和catalina.home一样
        String base = System.getProperty(Globals.CATALINA_BASE_PROP);
        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());
    }
}

Bootstrap.main启动流程

Main方法中,初始化BootStrap实例,根据参数命令来判断是启动还是终止Tomcat。

启动容器三个分支方法

  1. Bootstrap.init() 初始化三种类加载器,实例化Catalina
  2. Bootstrap.load()  加载server.xml配置,并初始化组件
  3. Bootstrap.start()  启动server及各组件,监听shutdown事件
    private static volatile Bootstrap daemon = null;
    public static void main(String args[]) {
       
        synchronized (daemonLock) {
            if (daemon == null) {
                // Don't set daemon until init() has completed
                Bootstrap bootstrap = new Bootstrap();
                try {
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                // When running as a service the call to stop will be on a new
                // thread so make sure the correct class loader is used to
                // prevent a range of class not found exceptions.
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        try {
            String command = "start";
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                args[args.length - 1] = "start";
                daemon.load(args);
                daemon.start();
            } else if (command.equals("stopd")) {
                args[args.length - 1] = "stop";
                daemon.stop();
            } else if (command.equals("start")) {
                daemon.setAwait(true);
                daemon.load(args);
                daemon.start();
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
            } else if (command.equals("stop")) {
                daemon.stopServer(args);
            } else if (command.equals("configtest")) {
                daemon.load(args);
                if (null == daemon.getServer()) {
                    System.exit(1);
                }
                System.exit(0);
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            // Unwrap the Exception for clearer error reporting
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }
    }

Bootstrap.init

org.apache.catalina.startup.Bootstrap#init()

实例化三种类型类加载器,实例化 Catalina 实例

    private Object catalinaDaemon = null;// catalina实例

    ClassLoader commonLoader = null;
    ClassLoader catalinaLoader = null;
    ClassLoader sharedLoader = null; 

 /**
     * Initialize daemon.
     * @throws Exception Fatal initialization error
     */
    public void init() throws Exception {

        // 实例化上边三种加载器
        initClassLoaders();

        // 设置当前启动线程的类加载器
        Thread.currentThread().setContextClassLoader(catalinaLoader);

        // 如果启动了安全管理SecurityManager,加载一些类,忽略这部分
        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
        if (log.isDebugEnabled())
            log.debug("Loading startup class");

        // 反射用catalinaLoader来实例化Catalina实例
        Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
        Object startupInstance = startupClass.getConstructor().newInstance();

        //System.out.println("输出bootstrap");
        //System.out.println(this.getClass().getName());
        //System.out.println(this.getClass().getClassLoader().toString());
        //Class<?> aClass = startupInstance.getClass();
        //System.out.println("输出catalina");
        //System.out.println(aClass.getName());
        //System.out.println(aClass.getClassLoader().toString());

        // 反射将sharedLoader设置为catalinaLoader的父类加载器
        if (log.isDebugEnabled())
            log.debug("Setting startup class properties");
        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

org.apache.catalina.startup.Bootstrap#initClassLoaders

   private void initClassLoaders() {
        try {
            // 创建三个类型的类加载器,默认配置下,三个是一个对象
            commonLoader = createClassLoader("common", null);
            if (commonLoader == null) {
                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);
        }
    }

// 删除了好多代码,看个大概意思
  private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
        // 取配置文件的资源路径
        String value = CatalinaProperties.getProperty(name + ".loader");
        if ((value == null) || (value.equals("")))
            return parent;

        List<Repository> repositories = new ArrayList<>();
        String[] repositoryPaths = getPaths(value);
        for (String repository : repositoryPaths) {
            if (repository.endsWith(".jar")) {
                repositories.add(new Repository(repository, RepositoryType.JAR));
            } else {
                repositories.add(new Repository(repository, RepositoryType.DIR));
            }
        }
        return ClassLoaderFactory.createClassLoader(repositories, parent);
    }

不再往下贴代码了,大概一说,创建三个类型的加载器

CatalinaProperties.getProperty(name + ".loader");查询配置文件中配置的加载器要加载的资源路径(配置文件key为common.loader/server.loader/shared.loader)

 配置文件路径优先级是:

  1. 先查系统参数配置的catalina.config路径,没有走2
  2. CATALINA_HOME/conf/catalina.properties,没有走3
  3. 包内的/org/apache/catalina/startup/catalina.properties

如果没有配置,使用参数中的父类加载器,默认配置下server.loader/shared.loader都为空,所以三个加载器是一个实例

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"
server.loader=
shared.loader=

 为什么需要三种类加载器?

tomcat的加载器不止三种,还有加载/webapp目录下的WebAppClassLoader、加载jsp的JsperClassLoader,类加载器架构如下图:

因为一个完善的Wen服务器要解决如下问题:

  • 部署在同一服务器上的web应用使用的类库需要相互隔离,可能都会依赖同一第三方类库的不同版本
  • 部署在同一服务器上的web应用使用的类库也可能需要相互共享,同一版本的Spring被多个应用共享
  • 服务器不受应用程序的影响,服务器使用的类库应该和应用程序的类库独立
  • Jsp的热替换,对于jsp的运行时修改频率远远大于class文件,为了支持热替换

因此,在tomcat中提供多种classpath路径来存放类库,对应四种加载器,如下

  • /common/*   被tomcat和所有web应用共享
  • /server/*       被tomcat使用,web应用不可见
  • /shared/*      tomcat不可见,web应用共享
  • /WEB-INF/*  仅被当前web应用使用

查看tomcat目录发现并没有前三个目录,因为默认配置下,server.loader和share.loader都为空,只创建一个加载器实例,所以自然将三个目录合并到/lib目录,tomcat团队做了简化,如果不能满足需要,可以修改配置并将目录拆分,启用上述加载器架构

 

 Bootstrap.load

直接反射调用catalina实例的load方法

   private void load(String[] arguments) throws Exception {

        // Call the load() method
        String methodName = "load";
        Object param[];
        Class<?> paramTypes[];
        if (arguments==null || arguments.length==0) {
            paramTypes = null;
            param = null;
        } else {
            paramTypes = new Class[1];
            paramTypes[0] = arguments.getClass();
            param = new Object[1];
            param[0] = arguments;
        }
        Method method =
            catalinaDaemon.getClass().getMethod(methodName, paramTypes);
        if (log.isDebugEnabled()) {
            log.debug("Calling startup class " + method);
        }
        method.invoke(catalinaDaemon, param);
    }

继续来看Catalina.load做了什么 

创建server.xml解析器,解析,初始化各组件,解析器Digester 这部分源码拆到下次详细看

   /**
     * Start a new server instance.
      ...省略一些代码,留着主要的
     */
    public void load() {

        if (loaded) {
            return;
        }
        loaded = true;

        // 检查java.io.tmpdir路径是否有效
        // 我的默认是C:\Users\lxy\AppData\Local\Temp\,路径检查是否是有效路径
        initDirs();

        // 创建Digester前设置一些使用的参数
        initNaming();

        // 解析server.xml的类
        Digester digester = createStartDigester();

        InputSource inputSource = null;
        InputStream inputStream = null;
        // 获取conf/server.xml
        File file = configFile();
        inputStream = new FileInputStream(file);
        inputSource = new InputSource(file.toURI().toURL().toString());
        
        // 开始解析
        inputSource.setByteStream(inputStream);
        digester.push(this);// 关联catalina和解析器
        digester.parse(inputSource);

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());

        initStreams();

        // Start the new server
        getServer().init();
    }

为什么Bootstrap类要反射调用Catalina

Bootstrap类中实例化Catalina或者调用Catalina的load或即将看到的start方法都是反射调用

我认为是因为为了使catalina类和启动类bootstrap不使用同一个加载器,当前启动类Bootstrap的类加载器是AppClassLoader,使真正的启动类Catalina都使用初始化的catalinaClassLoader加载

我在Bootstrap#init()方法中加了几行输出(上边贴的代码有),输出bootstrap和catalina实例的加载器

源码中因为两个都在一个包下,因此执行结果加载器是一个实例,如图

但是我们找一个我们平时发包使用的tomcat,会发现,tomcat把启动类bootstrap.java和catalina.java拆了,不在一个路径下

两个类这时候跑的时候就不是一个加载器了,测试一下,把源码bootstrap.java编译并替换到bootstrap.jar中,startup.bat运行截图如下

可以看到不是一个加载器,所以是为了使catalina类实例启动时涉及的类都由初始化的加载器catalinaClassLoader加载

 Bootstrap.start

即反射调用Catalina.start()

  public void start() throws Exception {
        if (catalinaDaemon == null) {
            init();
        }

        Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
        method.invoke(catalinaDaemon, (Object [])null);
    }

Catalina.start

  1. 启动Server,这个本文暂不跟代码
  2. 注册shutdown钩子,即注册一个线程任务,在jvm结束时调用(main结束时),如果server未停止调用stop方法停止
  3. 监听等待shutdown端口,监听到shutdown命令后调用Server.stop(),销毁容器
   /**
     * Start a new server instance.
     */
    public void start() {

        if (getServer() == null) {
            load();
        }

        if (getServer() == null) {
            log.fatal("Cannot start server. Server instance is not configured.");
            return;
        }

        try {
            // 1、启动Server
            getServer().start();
        } catch (LifecycleException e) {
            getServer().destroy();
            return;
        }

        // Register shutdown hook
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }

           // 2、注册shutdown钩子,main结束时调用,如果server未停止调用stop方法停止
            Runtime.getRuntime().addShutdownHook(shutdownHook);

            // If JULI is being used, disable JULI's shutdown hook since
            // shutdown hooks run in parallel and log messages may be lost
            // if JULI's hook completes before the CatalinaShutdownHook()
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }

        if (await) {
            // main线程等待,等待接收shutdown命令,接受到则跳出阻塞
            await();
            // 执行Server.stop();
            stop();
        }
    }

注册钩子CatalinaShutdownHook

即一个线程,判断当前server是否已被停止,如果还没则调用stop(),在Main结束时调用

    protected class CatalinaShutdownHook extends Thread {

        @Override
        public void run() {
            try {
                if (getServer() != null) {
                    Catalina.this.stop();
                }
            } catch (Throwable ex) {
                ExceptionUtils.handleThrowable(ex);
                log.error(sm.getString("catalina.shutdownHookFail"), ex);
            } finally {
                // If JULI is used, shut JULI down *after* the server shuts down
                // so log messages aren't lost
                LogManager logManager = LogManager.getLogManager();
                if (logManager instanceof ClassLoaderLogManager) {
                    ((ClassLoaderLogManager) logManager).shutdown();
                }
            }
        }
    }

 

await监听shutdown端口

在server.xml刚开头

8605端口就是shutdown端口,后边的属性就是终止命令,服务启动后服务器会监听8605端口,如果这个端口接收到了"SHUTDOWN"这个字符串,那么就会终止Server

   //org.apache.catalina.core.StandardServer#await
   // 为方便阅读,删除了部分无关代码,日志等
 @Override
    public void await() {
        if (port == -2) {
            // shutdown端口配置为-2,启动完Server直接再终止Server
            return;
        }
        if (port==-1) {
            // 配置为-1,则不再监听shutdown端口
            try {
                awaitThread = Thread.currentThread();
                while(!stopAwait) {
                    try {
                        Thread.sleep( 10000 );
                    } catch( InterruptedException ex ) {
                        // continue and check the flag
                    }
                }
            } finally {
                awaitThread = null;
            }
            return;
        }

        // 开启socket监听server.xml中的shutdown端口
        try {
            awaitSocket = new ServerSocket(port, 1,
                    InetAddress.getByName(address));
        } catch (IOException e) {
            return;
        }

        try {
            awaitThread = Thread.currentThread();

            // Loop waiting for a connection and a valid command
            while (!stopAwait) {
                ServerSocket serverSocket = awaitSocket;
                if (serverSocket == null) {
                    break;
                }

                Socket socket = null;
                StringBuilder command = new StringBuilder();
              
               InputStream stream;
               long acceptStartTime = System.currentTimeMillis();
               try {
                    // accept阻塞监听端口
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
               } catch (SocketTimeoutException ste) {
                    continue;
               }
                
                // 从流中读取字符串,如果命令是"SHUTDOWN"则,跳出循环,开始终止服务器
               // shutdown变量是取server.xml中Server的shutdown属性
               boolean match = command.toString().equals(shutdown);
                if (match) {
                    log.info(sm.getString("standardServer.shutdownViaPort"));
                    break;
                } 
            }
}

 服务器端口监听到了shutdown命令后,开始执行Catalina.stop()

    public void stop() {
        try {
            if (useShutdownHook) {
                // 移除shutdown钩子,这个stop方法会停止server,不需要钩子再次执行
                Runtime.getRuntime().removeShutdownHook(shutdownHook);
            }
        } catch (Throwable t) {
            ExceptionUtils.handleThrowable(t);
        }

        // Shut down the server
        try {
            Server s = getServer();
            LifecycleState state = s.getState();
            if (LifecycleState.STOPPING_PREP.compareTo(state) <= 0
                    && LifecycleState.DESTROYED.compareTo(state) >= 0) {
            } else {
                s.stop();
                s.destroy();
            }
        } catch (LifecycleException e) {
            log.error("Catalina.stop", e);
        }

Bootstrap的stop流程

在Bootstrap.main中默认没有参数走的是start,stop走stop流程

org.apache.catalina.startup.Catalina#stopServer(java.lang.String[])

  public void stopServer(String[] arguments) {

        if (arguments != null) {
            arguments(arguments);
        }

        Server s = getServer();
        if (s == null) {
            // 没有server,需要创建个Digester解析一下server.xml,解析出shutdown端口等server信息
            Digester digester = createStopDigester();
            File file = configFile();
            try (FileInputStream fis = new FileInputStream(file)) {
                InputSource is =
                    new InputSource(file.toURI().toURL().toString());
                is.setByteStream(fis);
                digester.push(this);
                digester.parse(is);
            } catch (Exception e) {
                log.error("Catalina.stop: ", e);
                System.exit(1);
            }
        } else {
            // server已经存在是什么情况,启动后的代码里直接再次调用了main方法吗?
            try {
                s.stop();
                s.destroy();
            } catch (LifecycleException e) {
                log.error("Catalina.stop: ", e);
            }
            return;
        }

        // 连接server的shutdown端口,发送一个SHUTDOWN字符串,那边接受到就关闭Server
        s = getServer();
        if (s.getPort()>0) {
            try (Socket socket = new Socket(s.getAddress(), s.getPort());
                    OutputStream stream = socket.getOutputStream()) {
                String shutdown = s.getShutdown();
                for (int i = 0; i < shutdown.length(); i++) {
                    stream.write(shutdown.charAt(i));
                }
                stream.flush();
            } catch (ConnectException ce) {
                System.exit(1);
            } catch (IOException e) {
                System.exit(1);
            }
        } else {
            System.exit(1);
        }
    }

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值