Tomcat源码:启动类Bootstrap与Catalina的加载

参考资料:

《Tomcat源码解析系列(一)Bootstrap》

《Tomcat源码解析系列(二)Catalina》

《Tomcat - 启动过程:初始化和启动流程》

《Tomcat - 启动过程:类加载机制详解》

《Tomcat - 启动过程:Catalina的加载》

相关资料:

《Tomcat:servlet与servlet容器》

        写在开头:本文为个人学习笔记,内容比较随意,夹杂个人理解,如有错误,欢迎指正。

前言

        本文开始我们将会介绍tomcat的源码,整个流程会按照tomcat的启动顺序来开展,在此之前建议先阅读下前文理清servlet与servlet容器之间的关系。   

        tomcat的源码可以在管网下载:传送门

        在tomcat的bin目录下有两个启动tomcat的文件, 一个是startup.bat, 它用于windows环境下启动tomcat; 另一个是startup.sh, 它用于linux环境下tomcat的启动. 两个文件中的逻辑是一样的, 我们只分析其中的startup.bat。而startup.bat文件实际上就做了一件事情: 启动catalina.bat。

        catalina.bat中下面这段指定了tomcat的启动类为Bootstrap这个类,catalina.bat最终执行了Bootstrap类中的main方法来启动tomcat。(catalina.bat的详细内容可以参考这篇文章《详解Tomcat系列(一)-从源码分析Tomcat的启动》,本文就不做深入解析了)

set _EXECJAVA=%_RUNJAVA%
set MAINCLASS=org.apache.catalina.startup.Bootstrap
set ACTION=start

            tomcat主要分为连接器与容器两部分,连接器负责接收与转发请求,容器负责响应请求,我们将会先从容器开始介绍整个tomcat的内容。

 

目录

一、启动类Bootstrap

        1、main

        2、类加载器的初始化

        2.1、commonLoader

        2.2、catalinaLoader与sharedLoader 

        3、init

        4、load与start

二、Catalina初始化

        1、load

        1.1、第一段

        1.2、第三段

         2、start

补充

        tomcat的类加载器模式


一、启动类Bootstrap

        首先来看下整个启动过程,我们可以看到Bootstrap作为启动入口首先进行了初始化方法init然后load方法加载了Catalina,下面我们就开始介绍Bootstrap。

        1、main

        Bootstrap的main方法首先会创建一个 Bootstrap 对象,调用它的 init 方法初始化,然后根据启动参数,调用Bootstrap对象的不同方法,默认模式为start,该模式下将会先后调用load与start方法。

    private static final Object daemonLock = new Object();
    private static volatile Bootstrap daemon = null;
    
    public static void main(String args[]) {
        // 创建一个 Bootstrap 对象
        synchronized (daemonLock) {
            if (daemon == null) {
                Bootstrap bootstrap = new Bootstrap();
                try {
                    // 调用init方法初始化
                    bootstrap.init();
                } catch (Throwable t) {
                    handleThrowable(t);
                    t.printStackTrace();
                    return;
                }
                daemon = bootstrap;
            } else {
                Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);
            }
        }

        // 根据启动参数,分别调用 Bootstrap 对象的不同方法
        try {
            // 默认参数为start
            String command = "start"; 
            if (args.length > 0) {
                command = args[args.length - 1];
            }

            if (command.equals("startd")) {
                ...
            } else if (command.equals("stopd")) {
                ...
            } 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")) {
               ...
            } else if (command.equals("configtest")) {
                ...                    
            } else {
                log.warn("Bootstrap: command \"" + command + "\" does not exist.");
            }
        } catch (Throwable t) {
            if (t instanceof InvocationTargetException &&
                    t.getCause() != null) {
                t = t.getCause();
            }
            handleThrowable(t);
            t.printStackTrace();
            System.exit(1);
        }

    }
}

        2、类加载器的初始化

        2.1、commonLoader

        init方法内部首先调用了initClassLoaders方法,看名字可以得得知这里将要进行的便是类加载器的初始化,类加载器的内容在我的《Java8之类的加载》《Java8之类加载机制class源码分析》中有过详细介绍,这里就不做赘述了,不了解的朋友可以先看完这两篇文章在继续看本文。

public void init() throws Exception {
    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();
    // 其余代码
}

         而在initClassLoaders中,又调用了createClassLoader进行了类加载器的创建,这里我们关注下传入的参数为common与null,下文将会用到。

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

private void initClassLoaders() {
    commonLoader = createClassLoader("common", null);
        if (commonLoader == null) {
            commonLoader = this.getClass().getClassLoader();
        }
        // 其余代码
}

        来到createClassLoader方法,方法中第一步就是根据传入的参数name从Catalina.Properties文件里找对应的loader,上文传入的参数为common,因此这里寻找的是common.loader。

        我们找到catalina.property文件,其中对应的值如下,可以看到是个路径。

# Note: Values are enclosed in double quotes ("...") in case either the
#       ${catalina.base} path or the ${catalina.home} path contains a comma.
#       Because double quotes are used for quoting, the double quote character
#       may not appear in a path.
common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

         获取到common.loader后再将其构造成Repository列表,再将Repository列表传入ClassLoaderFactory.createClassLoader 方法。

private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    // 从CatalinaProperties文件里找common.loader,
    String value = CatalinaProperties.getProperty(name + ".loader");
    // 如果name+.loader对应值为空,直接返回父类加载器,后文将会用到
    if ((value == null) || (value.equals("")))
        return parent;

    value = replace(value);

    List<Repository> repositories = new ArrayList<>();
    // 构造Repository列表
    String[] repositoryPaths = getPaths(value);

    for (String repository : repositoryPaths) {
        try {
            @SuppressWarnings("unused")
            URL url = new URL(repository);
            repositories.add(new Repository(repository, RepositoryType.URL));
            continue;
        } catch (MalformedURLException e) {
        }

        if (repository.endsWith("*.jar")) {
            repository = repository.substring
                (0, repository.length() - "*.jar".length());
            repositories.add(new Repository(repository, RepositoryType.GLOB));
        } else if (repository.endsWith(".jar")) {
            repositories.add(new Repository(repository, RepositoryType.JAR));
        } else {
            repositories.add(new Repository(repository, RepositoryType.DIR));
        }
    }
    // 将Repository列表传入ClassLoaderFactory.createClassLoader
    return ClassLoaderFactory.createClassLoader(repositories, parent);
}

        ClassLoaderFactory.createClassLoader中将会根据不同的类型采用不同的处理方案,最终创建URLClassLoader对象。

    public static ClassLoader createClassLoader(List<Repository> repositories,
                                                final ClassLoader parent)
        throws Exception {
        // 路径集合
        Set<URL> set = new LinkedHashSet<>();
        if (repositories != null) {
            // 遍历传入的路径
            for (Repository repository : repositories)  {
                // 根据不同的类型采用不同的处理方案,这里仅展示DIR与JAR的处理
                if (repository.getType() == RepositoryType.URL) {
                    ...
                } else if (repository.getType() == RepositoryType.DIR) {
                    File directory = new File(repository.getLocation());
                    directory = directory.getCanonicalFile();
                    if (!validateFile(directory, RepositoryType.DIR)) {
                        continue;
                    }
                    URL url = buildClassLoaderUrl(directory);
                    if (log.isDebugEnabled()) {
                        log.debug("  Including directory " + url);
                    }
                    set.add(url);
                } else if (repository.getType() == RepositoryType.JAR) {
                    File file=new File(repository.getLocation());
                    file = file.getCanonicalFile();
                    if (!validateFile(file, RepositoryType.JAR)) {
                        continue;
                    }
                    URL url = buildClassLoaderUrl(file);
                    if (log.isDebugEnabled()) {
                        log.debug("  Including jar file " + url);
                    }
                    set.add(url);
                } else if (repository.getType() == RepositoryType.GLOB) {
                    ...
                }
            }
        }
        final URL[] array = set.toArray(new URL[0]);
        // 使用URLClassLoader加载这些路径下的类
        return AccessController.doPrivileged(
                new PrivilegedAction<URLClassLoader>() {
                    @Override
                    public URLClassLoader run() {
                        if (parent == null) {
                            return new URLClassLoader(array);
                        } else {
                            return new URLClassLoader(array, parent);
                        }
                    }
                });
    }

        到这里我们就走完了commonLoader的创建过程,其内容为URLClassLoader对象。

        2.2、catalinaLoader与sharedLoader 

        回到initClassLoaders方法,我们继续看catalinaLoader与sharedLoader的初始化,同样是调用createClassLoader方法,但父类加载器参数此时由null变为了commonLoader。

    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);
        }
    }

        Catalina.Properties中server.loader、shared.loader值为空,结合createClassLoader的内容这里将传入的父类加载器传了回去,因此实际上这三个类加载器都是同一个URLClassLoader对象。

server.loader=
shared.loader=
private ClassLoader createClassLoader(String name, ClassLoader parent)
    throws Exception {
    // 从CatalinaProperties文件里找common.loader,
    String value = CatalinaProperties.getProperty(name + ".loader");
    // 如果name+.loader对应值为空,直接返回父类加载器,后文将会用到
    if ((value == null) || (value.equals("")))
        return parent;
    // 其余代码
}

        至此,Bootstrap类中的类加载器初始化完毕。

        3、init

        使用上一步创建的catalinaClassLoader 加载了org.apache.catalina.startup.Catalina类,并创建了一个对象catalinaDaemon,然后通过反射调用这个对象的 setParentClassLoader 方法,传入的参数是 sharedClassLoader。

public void init() throws Exception {
    // 初始化classloader(包括catalinaLoader),下文将具体分析
    initClassLoaders();
    // 设置当前的线程的contextClassLoader为catalinaLoader
    Thread.currentThread().setContextClassLoader(catalinaLoader);
    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // 通过catalinaLoader加载Catalina,并初始化startupInstance 对象
    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.getConstructor().newInstance();
    // 通过反射调用了setParentClassLoader 方法
    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);
    catalinaDaemon = startupInstance;
}

        4、load与start

       load与start都是通过上一步获取到的catalinaDaemon对象反射调用catalina类的load与start方法,这两个过程我们会在后续的catalina内容中介绍。

private void load(String[] arguments) throws Exception {
    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); 
    // 反射调用catalina的load方法
    method.invoke(catalinaDaemon, param);
}
    public void start() throws Exception {
        if (catalinaDaemon == null) {
            init();
        }
        Method method = catalinaDaemon.getClass().getMethod("start", (Class [])null);
        method.invoke(catalinaDaemon, (Object [])null);
    }

二、Catalina初始化

        上文中Bootstrap类的load与start方法实质上就是反射调用catalina类的load与start方法,本节我们来介绍下该类。

        1、load

        load(String[])本质上还是调用了load方法

public void load(String args[]) {
    try {
        // 处理命令行的参数
        if (arguments(args)) { 
            load();
        }
    } catch (Exception e) {
        e.printStackTrace(System.out);
    }
}

        load() 方法的逻辑挺清晰的,大概可以分为三段,第一个 try-catch 之前算一段,第一个 try-catch 算第二段,第一个 try-catch 到第二个 try-catch 结束算第三段。 

public void load() {
        // 如果已经加载则退出
        if (loaded) {
            return;
        }
        loaded = true;
        long t1 = System.nanoTime();
        initDirs();
        initNaming();
        ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));
        File file = configFile();
        // 创建digester对象,用于解析server.xml
        Digester digester = createStartDigester();
        try (ConfigurationSource.Resource resource = ConfigFileLoader.getSource().getServerXml()) {
            InputStream inputStream = resource.getInputStream();
            InputSource inputSource = new InputSource(resource.getURI().toURL().toString());
            inputSource.setByteStream(inputStream);
            digester.push(this);
            // 解析server.xml
            digester.parse(inputSource);
        } catch (Exception e) {
                // ...
            }
            return;
        }
        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
        initStreams();
        // 调用StandardServer.init启动Server
        try {
            getServer().init();
        } catch (LifecycleException e) {
            // ...
        }
        long t2 = System.nanoTime();
    }

        1.1、第一段

        首先是initDirs与initNaming,其中initDirs无内容,且备注为“Will be removed in Tomcat 10 onwards”,initNaming方法主要是些额外系统变量的设置,因此都无需关注。

        这一行代码代码的作用是设置Tomcat要加载的配置的配置源,也就是 conf 目录下的 server.xml 文件。

ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(Bootstrap.getCatalinaBaseFile(), getConfigFile()));

       接下来这一行创建了一个 Digester对象,这个对象是用来解析 server.xml 文件的

Digester digester = createStartDigester();

        从注释和大部分相似的代码可以看出,Digester 就是用来解析 server.xml 并创建对应的默认实现类对象的。比如碰到 <Server> 标签就创建默认的 StarndardServer 类对象。 


    protected Digester createStartDigester() {
        long t1=System.currentTimeMillis();
        Digester digester = new Digester();
        digester.setValidating(false);
        digester.setRulesValidation(true);
         ……
        digester.addObjectCreate("Server",
                                 "org.apache.catalina.core.StandardServer",
                                 "className");
        digester.addSetProperties("Server");
        digester.addSetNext("Server",
                            "setServer",
                            "org.apache.catalina.Server");
        digester.addObjectCreate("Server/GlobalNamingResources",
                                 "org.apache.catalina.deploy.NamingResourcesImpl");
        digester.addSetProperties("Server/GlobalNamingResources");
        digester.addSetNext("Server/GlobalNamingResources",
                            "setGlobalNamingResources",
                            "org.apache.catalina.deploy.NamingResourcesImpl");
       ……
        return digester;
    }

        第二段中的内容其实就是调用Digester对象来解析server.xml,内部使用了SAXParser来解析 ,解析完了之后,xml 里定义的各种标签就有对应的实现类对象了。

        1.2、第三段

        第三段中使用了getServer()方法,其获取的是catalina类中的成员变量,其赋值是在 digester.parse解析xml的时候完成的。

protected Server server = null;

         最后调用了server实现类中的init方法,server的内容我们会在后续进行介绍。

        getServer().setCatalina(this);
        getServer().setCatalinaHome(Bootstrap.getCatalinaHomeFile());
        getServer().setCatalinaBase(Bootstrap.getCatalinaBaseFile());
        initStreams();
        try {
            getServer().init();
        } catch (LifecycleException e) {
            // ...
        }

         2、start

        在 load 方法之后,Tomcat 就初始化了一系列的组件,接着就可以调用 start 方法进行启动了。

    public void start() {
        if (getServer() == null) {
            load();
        }
        if (getServer() == null) {
            log.fatal(sm.getString("catalina.noServer"));
            return;
        }
        long t1 = System.nanoTime();
        try {
            getServer().start();
        } catch (LifecycleException e) {
            // ...
            return;
        }
        long t2 = System.nanoTime();
        if (useShutdownHook) {
            if (shutdownHook == null) {
                shutdownHook = new CatalinaShutdownHook();
            }
            Runtime.getRuntime().addShutdownHook(shutdownHook);
            LogManager logManager = LogManager.getLogManager();
            if (logManager instanceof ClassLoaderLogManager) {
                ((ClassLoaderLogManager) logManager).setUseShutdownHook(
                        false);
            }
        }
        if (await) {
            await();
            stop();
        }
    }

         整段代码逻辑还是比较好理解的,核心过程就是getServer().start()调用Server对象的 start() 方法来启动 Tomcat。Server相关的内容我们会在后续进行介绍,本文暂不做讨论。

        这里提一下ShutDownHook关闭钩子,关闭钩子的内容我之前有过介绍,可以看下这篇文章《java8:关闭钩子shutdown hook》,这里实际上是调用 Catalina 对象的 stop 方法来停止 tomcat,而其内部又继续调用了Server对象的stop方法。

        最后就进入 if 语句了,await 是在 Bootstrap 里调用的时候设置为 true 的,也就是本文开头的时候提到的三个方法中的一个。await 方法的作用是停住主线程,等待用户输入shutdown 命令之后,停止等待,之后 main 线程就调用 stop 方法来停止Tomcat。

补充

        tomcat的类加载器模式

        了解类加载器的朋友都知道Java默认的类加载机制是通过双亲委派模型来实现的(不了解的可以看下我这篇文章《Java8之类的加载》《Java8之类加载机制class源码分析》),不过tomcat却没有使用这种模式,下面我们来简单说明下原因。

        原因在于一个Tomcat容器允许同时运行多个Web程序,每个Web程序依赖的类又必须是相互隔离的。

        如果Tomcat使用双亲委派模式来加载类的话,将导致Web程序依赖的类变为共享的。举个例子,假如我们有两个Web程序,一个依赖A库的1.0版本,另一个依赖A库的2.0版本,他们都使用了类xxx.xx.Clazz,其实现的逻辑因类库版本的不同而结构完全不同。那么这两个Web程序的其中一个必然因为加载的Clazz不是所使用的Clazz而出现问题。

        为了解决这个问题,tomcat采用了如下的设计方式

                       

         最上面的三个类加载器我们已经了解过了,重点在于下面这些,即我们在initClassLoaders方法中创建的三个类加载器,其作用分别如下:

        Common类加载器,负责加载Tomcat和Web应用都复用的类

        (1)Catalina类加载器,负责加载Tomcat专用的类,而这些被加载的类在Web应用中将不可见

        (2)Shared类加载器,负责加载Tomcat下所有的Web应用程序都复用的类,而这些被加载的类在Tomcat中将不可见

        (2.1)WebApp类加载器,负责加载具体的某个Web应用程序所使用到的类,而这些被加载的类在Tomcat和其他的Web应用程序都将不可见

        (2.2)Jsp类加载器,每个jsp页面一个类加载器,不同的jsp页面有不同的类加载器,方便实现jsp页面的热插拔

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值