写点什么

【Tomcat 源码分析 】 类加载机制的源码解读

作者:派大星
  • 2024-09-09
    辽宁
  • 本文字数:10441 字

    阅读完需:约 34 分钟

前言

继前文深入剖析双亲委派机制之后,本文将引直接走进具体的代码实现,一探其真正的实现思路。

源码阅读

Tomcat 启动的起点在于 Bootstrap 类的 main()方法。在 main()方法执行之前,其静态代码块(static{})会率先被执行。因此,我们将首先深入探讨静态代码块的运行机制,然后再分析 main()方法的执行流程。

Bootstrap.static{}


static {    // 获取用户目录    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 所在目录的上一级目录获取    if (homeFile == null) {        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) {        File f = new File(userDir);        try {            homeFile = f.getCanonicalFile();        } catch (IOException ioe) {            homeFile = f.getAbsoluteFile();        }    }
    // 设置 CATALINA_HOME 属性    catalinaHomeFile = homeFile;    System.setProperty(Globals.CATALINA_HOME_PROP, catalinaHomeFile.getPath());
    // 获取 CATALINA_BASE,若未设置,则使用 CATALINA_HOME 作为 CATALINA_BASE    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;    }    // 设置 CATALINA_BASE 属性    System.setProperty(Globals.CATALINA_BASE_PROP, catalinaBaseFile.getPath());}

复制代码

在启动的初始阶段,Tomcat 需要确定自身的根目录(catalina.home)和工作目录(catalina.base)。 这段代码描述了寻找这两个关键路径的逻辑:

  1. 探寻根目录: 首先,代码会尝试从系统环境变量中获取 catalina.home 的值。 若未找到,则会进一步探寻 bootstrap.jar 所在目录的上一级目录,并将该目录作为 catalina.home。 最终,若仍无法确定 catalina.home,则将当前用户目录(user.dir)作为默认值。

  2. 定位工作目录: 随后,代码会尝试从系统环境变量中获取 catalina.base 的值。 若未找到,则将 catalina.home 的值作为 catalina.base

最后,代码会将最终确定的 catalina.homecatalina.base 路径信息设置为系统属性,以便在后续的启动流程中使用。

这段代码如同为 Tomcat 构建一座稳固的基石,它负责加载并设置 catalina.homecatalina.base 相关的信息,为后续的启动流程奠定基础。

main()

Tomcat 的 main 方法可概括为两个主要阶段:**初始化 (**init) 和 加载与启动 (load+start);。

public static void main(String args[]) {    // 初始化阶段  main方法第一次执行的时候,daemon肯定为null,所以直接new了一个Bootstrap对象,然后执行其init()方法    if (daemon == null) {        Bootstrap bootstrap = new Bootstrap();        try {            bootstrap.init();        } catch (Throwable t) {            handleThrowable(t);            t.printStackTrace();            return;        }        //daemon守护对象设置为bootstrap        daemon = bootstrap;    } else {        // 当作为服务运行时,对stop的调用将在新线程上进行,        // 因此确保使用正确的类加载器以防止一系列类未找到异常。        Thread.currentThread().setContextClassLoader(daemon.catalinaLoader);    }
    // 加载与启动阶段    // 执行守护对象的load方法和start方法    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) {        // 展开异常以获得更清晰的错误报告        if (t instanceof InvocationTargetException &&                t.getCause() != null) {            t = t.getCause();        }        handleThrowable(t);        t.printStackTrace();        System.exit(1);    }}

复制代码

我们点到init()里面去看看~

public void init() throws Exception {    // 非常关键的地方,初始化类加载器s,后面我们会详细具体地分析这个方法    initClassLoaders();
    // 设置上下文类加载器为catalinaLoader,这个类加载器负责加载Tomcat专用的类    Thread.currentThread().setContextClassLoader(catalinaLoader);    // 暂时略过,后面会讲    SecurityClassLoad.securityClassLoad(catalinaLoader);
    // 使用catalinaLoader加载我们的Catalina类    // Load our startup class and call its process() method    if (log.isDebugEnabled())        log.debug("Loading startup class");    Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");    Object startupInstance = startupClass.getConstructor().newInstance();
    // 设置Catalina类的parentClassLoader属性为sharedLoader    // Set the shared extensions class loader    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守护对象为刚才使用catalinaLoader加载类、并初始化出来的Catalina对象    catalinaDaemon = startupInstance;}
复制代码

initClassLoaders 方法是 Tomcat 类加载机制的核心,它负责初始化 Tomcat 的各个类加载器,构建起 Tomcat 独特的类加载体系。通过分析这个方法,我们可以清晰地验证上一节提到的 Tomcat 类加载图。

private void initClassLoaders() {    try {        // 创建commonLoader,如果未创建成果的话,则使用应用程序类加载器作为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,父类加载器为commonLoader        catalinaLoader = createClassLoader("server", commonLoader);        // 创建sharedLoader,父类加载器为commonLoader        sharedLoader = createClassLoader("shared", commonLoader);    } catch (Throwable t) {        // 如果创建的过程中出现异常了,日志记录完成之后直接系统退出        handleThrowable(t);        log.error("Class loader creation threw exception", t);        System.exit(1);    }}
复制代码

createClassLoader 方法是 Tomcat 类加载器创建的底层方法,它负责根据配置文件 catalina.properties 中的配置信息来创建具体的类加载器实例。 CatalinaProperties.getProperty("xxx") 方法用于从 conf/catalina.properties 文件中获取指定属性的值,为 createClassLoader 方法提供必要的配置信息。

private ClassLoader createClassLoader(String name, ClassLoader parent)    throws Exception {    // 获取类加载器待加载的位置,如果为空,则不需要加载特定的位置,使用父类加载返回回去。    String value = CatalinaProperties.getProperty(name + ".loader");    if ((value == null) || (value.equals("")))        return parent;    // 替换属性变量,比如:${catalina.base}、${catalina.home}    value = replace(value);
    List<Repository> repositories = new ArrayList<>();
   // 解析属性路径变量为仓库路径数组    String[] repositoryPaths = getPaths(value);
    // 对每个仓库路径进行repositories设置。我们可以把repositories看成一个个待加载的位置对象,可以是一个classes目录,一个jar文件目录等等    for (String repository : repositoryPaths) {        // Check for a JAR URL repository        try {            @SuppressWarnings("unused")            URL url = new URL(repository);            repositories.add(                    new Repository(repository, RepositoryType.URL));            continue;        } catch (MalformedURLException e) {            // Ignore        }
        // Local repository        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));        }    }    // 使用类加载器工厂创建一个类加载器    return ClassLoaderFactory.createClassLoader(repositories, parent);}
复制代码

我们来分析一下ClassLoaderFactory.createClassLoader--类加载器工厂创建类加载器。

public static ClassLoader createClassLoader(List<Repository> repositories,                                            final ClassLoader parent)    throws Exception {
    if (log.isDebugEnabled())        log.debug("Creating new class loader");
    // Construct the "class path" for this class loader    Set<URL> set = new LinkedHashSet<>();    // 遍历repositories,对每个repository进行类型判断,并生成URL,每个URL我们都要校验其有效性,有效的URL我们会放到URL集合中    if (repositories != null) {        for (Repository repository : repositories)  {            if (repository.getType() == RepositoryType.URL) {                URL url = buildClassLoaderUrl(repository.getLocation());                if (log.isDebugEnabled())                    log.debug("  Including URL " + url);                set.add(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) {                File directory=new File(repository.getLocation());                directory = directory.getCanonicalFile();                if (!validateFile(directory, RepositoryType.GLOB)) {                    continue;                }                if (log.isDebugEnabled())                    log.debug("  Including directory glob "                        + directory.getAbsolutePath());                String filenames[] = directory.list();                if (filenames == null) {                    continue;                }                for (int j = 0; j < filenames.length; j++) {                    String filename = filenames[j].toLowerCase(Locale.ENGLISH);                    if (!filename.endsWith(".jar"))                        continue;                    File file = new File(directory, filenames[j]);                    file = file.getCanonicalFile();                    if (!validateFile(file, RepositoryType.JAR)) {                        continue;                    }                    if (log.isDebugEnabled())                        log.debug("    Including glob jar file "                            + file.getAbsolutePath());                    URL url = buildClassLoaderUrl(file);                    set.add(url);                }            }        }    }
    // Construct the class loader itself    final URL[] array = set.toArray(new URL[set.size()]);    if (log.isDebugEnabled())        for (int i = 0; i < array.length; i++) {            log.debug("  location " + i + " is " + array[i]);        }
    // 从这儿看,最终所有的类加载器都是URLClassLoader的对象~~    return AccessController.doPrivileged(            new PrivilegedAction<URLClassLoader>() {                @Override                public URLClassLoader run() {                    if (parent == null)                        return new URLClassLoader(array);                    else                        return new URLClassLoader(array, parent);                }            });}
复制代码

对 initClassLoaders 分析完,Tomcat 还会进行一项至关重要的安全措施,即 SecurityClassLoad.securityClassLoad 方法。 让我们深入探究这个方法,看看它在 Tomcat 的安全体系中扮演着怎样的角色。

public static void securityClassLoad(ClassLoader loader) throws Exception {    securityClassLoad(loader, true);}
static void securityClassLoad(ClassLoader loader, boolean requireSecurityManager) throws Exception {
    if (requireSecurityManager && System.getSecurityManager() == null) {        return;    }
    loadCorePackage(loader);    loadCoyotePackage(loader);    loadLoaderPackage(loader);    loadRealmPackage(loader);    loadServletsPackage(loader);    loadSessionPackage(loader);    loadUtilPackage(loader);    loadValvesPackage(loader);    loadJavaxPackage(loader);    loadConnectorPackage(loader);    loadTomcatPackage(loader);}
 private static final void loadCorePackage(ClassLoader loader) throws Exception {    final String basePackage = "org.apache.catalina.core.";    loader.loadClass(basePackage + "AccessLogAdapter");    loader.loadClass(basePackage + "ApplicationContextFacade$PrivilegedExecuteMethod");    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedForward");    loader.loadClass(basePackage + "ApplicationDispatcher$PrivilegedInclude");    loader.loadClass(basePackage + "ApplicationPushBuilder");    loader.loadClass(basePackage + "AsyncContextImpl");    loader.loadClass(basePackage + "AsyncContextImpl$AsyncRunnable");    loader.loadClass(basePackage + "AsyncContextImpl$DebugException");    loader.loadClass(basePackage + "AsyncListenerWrapper");    loader.loadClass(basePackage + "ContainerBase$PrivilegedAddChild");    loadAnonymousInnerClasses(loader, basePackage + "DefaultInstanceManager");    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntry");    loader.loadClass(basePackage + "DefaultInstanceManager$AnnotationCacheEntryType");    loader.loadClass(basePackage + "ApplicationHttpRequest$AttributeNamesEnumerator");}
复制代码

这段代码使用 catalinaLoader 加载了 Tomcat 源代码中各个专用类,这些类主要分布在以下几个包中:

  1. org.apache.catalina.core.*

  2. org.apache.coyote.*

  3. org.apache.catalina.loader.*

  4. org.apache.catalina.realm.*

  5. org.apache.catalina.servlets.*

  6. org.apache.catalina.session.*

  7. org.apache.catalina.util.*

  8. org.apache.catalina.valves.*

  9. javax.servlet.http.Cookie

  10. org.apache.catalina.connector.*

  11. org.apache.tomcat.*

至此,我们已经逐一分析了 init 方法中的关键方法,包括 initClassLoadersSecurityClassLoad.securityClassLoad 等,了解了 Tomcat 初始化阶段的各个步骤。

WebApp 类加载器

在深入探究 Tomcat 启动流程的过程中,我们发现似乎遗漏了一个关键角色——WebApp 类加载器。

WebApp 类加载器是每个 Web 应用独有的,而每个 Web 应用本质上就是一个 Context。 因此,我们理所当然地将目光投向了 Context 的实现类。 Tomcat 中,StandardContextContext 的默认实现,而 WebApp 类加载器正是诞生于 StandardContext 类的 startInternal() 方法中。

protected synchronized void startInternal() throws LifecycleException {    if (getLoader() == null) {        WebappLoader webappLoader = new WebappLoader(getParentClassLoader());        webappLoader.setDelegate(getDelegate());        setLoader(webappLoader);    }}
复制代码

这段代码的逻辑非常简洁,它在 WebApp 类加载器不存在的情况下,会创建一个新的 WebApp 类加载器,并将其设置为当前 Context 的加载器(setLoader)。

到这里,我们已经完成了对 Tomcat 启动过程和类加载机制的全面解析,从 Bootstrap 类开始,一步步深入,最终揭开了 WebApp 类加载器的面纱。

通过这个分析过程,我们不仅了解了 Tomcat 启动和类加载的具体步骤,更深刻地理解了 Tomcat 采用这种独特的多层级类加载机制的深层原因,以及这种设计带来的种种优势。

如有问题,欢迎微信搜索【码上遇见你】。

免费的Chat GPT可微信搜索【AI贝塔】进行体验,无限使用。

好了,本章节到此告一段落。希望对你有所帮助,祝学习顺利。

发布于: 刚刚阅读数: 5
用户头像

派大星

关注

微信搜索【码上遇见你】,获取更多精彩内容 2021-12-13 加入

微信搜索【码上遇见你】,获取更多精彩内容

评论

发布
暂无评论
【Tomcat源码分析 】 类加载机制的源码解读_tomcat源码解读_派大星_InfoQ写作社区