类加载机制的源码解读

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

前言

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

源码阅读

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.home 和 catalina.base 路径信息设置为系统属性,以便在后续的启动流程中使用。

这段代码如同为 Tomcat 构建一座稳固的基石,它负责加载并设置 catalina.home 和 catalina.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 方法中的关键方法,包括 initClassLoaders、SecurityClassLoad.securityClassLoad 等,了解了 Tomcat 初始化阶段的各个步骤。

WebApp 类加载器

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

WebApp 类加载器是每个 Web 应用独有的,而每个 Web 应用本质上就是一个 Context。 因此,我们理所当然地将目光投向了 Context 的实现类。 Tomcat 中,StandardContext 是 Context 的默认实现,而 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 采用这种独特的多层级类加载机制的深层原因,以及这种设计带来的种种优势。

责任编辑:武晓燕 来源: 码上遇见你
相关推荐

2021-01-06 09:01:05

javaclass

2021-07-05 06:51:43

Java机制类加载器

2023-10-31 16:00:51

类加载机制Java

2024-03-12 07:44:53

JVM双亲委托机制类加载器

2017-09-20 08:07:32

java加载机制

2017-03-08 10:30:43

JVMJava加载机制

2021-04-29 11:18:14

JVM加载机制

2009-02-03 09:42:53

JAVA类JVM指令forName方法

2024-09-04 09:47:21

2023-10-13 07:29:23

PixiJSRunner

2020-05-26 18:50:46

JVMAttachJava

2023-10-17 09:26:44

Java工具

2023-05-10 11:07:18

2024-12-02 09:01:23

Java虚拟机内存

2020-10-26 11:20:04

jvm类加载Java

2012-01-18 11:24:18

Java

2020-05-20 22:13:26

JVM加载机制虚拟机

2022-10-08 08:34:34

JVM加载机制代码

2021-09-24 08:10:40

Java 语言 Java 基础

2023-08-02 08:38:27

JVM加载机制
点赞
收藏

51CTO技术栈公众号