• Tomcat类加载器机制


    Tomcat为什么需要定制自己的ClassLoader:

    1、定制特定的规则:隔离webapp,安全考虑,reload热插拔

    2、缓存类

    3、事先加载

    要说Tomcat的Classloader机制,我们还得从Bootstrap开始。在BootStrap初始化的时候,调用了org.apache.catalina.startup.Bootstrap#initClassLoaders方法,这个方法里面创建了3个ClassLoader,它们分别是commonLoader,catalinaLoader,sharedLoader,其中catalinaLoader,sharedLoader的父亲加载器是commonLoader,initClassLoaders执行的过程中会执行createClassLoader,而createClassLoader是根据conf/catalina.properties文件中common.loader,server.loader,shared.loader的值来初始化,它的代码如下:

    默认情况下,这3个ClassLoader是同一个实例变量

    private ClassLoader createClassLoader(String name, ClassLoader parent)
        throws Exception {
    
        String value = CatalinaProperties.getProperty(name + ".loader");
        // 1 
        if ((value == null) || (value.equals("")))
            return parent;
    
        // 2
        value = replace(value);
    
        List<Repository> repositories = new ArrayList<Repository>();
    
        StringTokenizer tokenizer = new StringTokenizer(value, ",");
        while (tokenizer.hasMoreElements()) {
            String repository = tokenizer.nextToken().trim();
            if (repository.length() == 0) {
                continue;
            }
    
            // 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));
            }
        }
        // 3
        ClassLoader classLoader = ClassLoaderFactory.createClassLoader
            (repositories, parent);//这里返回的是一个UrlClassLoader实例
    
    
        return classLoader;
    
    }

    以上代码删除了与本篇无关的代码,下面我们分别来分析一下标注的地方:

    1. 标注1的代码(第5行)判断如果catalina.properties中没有配置对应的loader属性的话,直接返回父加载器,而默认情况下,server.loader,shared.loader为空,那么此时的catalinaLoader,sharedLoader其实是同一个ClassLoader.
    2. 标注2(第9行)的地方根据环境变量的配置替换字符串中的值.默认情况下,common.loader的值为common.loader=${catalina.base}/lib,${catalina.base}/lib/.jar,${catalina.home}/lib,${catalina.home}/lib/.jar,这里会将catalina.base和catalina.home用环境变量的值替换。
    3. 标注3(第46行)的代码最终调用org.apache.catalina.startup.ClassLoaderFactory#createClassLoader静态工厂方法创建了URLClassloader的实例,而具体的URL其实就是*.loader属性配置的内容,此外如果parent为null的话,则直接用系统类加载器

    上面分析了Tomcat在启动的时候,初始化的几个ClassLoader,接下来我们再来继续看看,这些ClassLoader具体都用在什么地方。

    我们接着来看org.apache.catalina.startup.Bootstrap#init方法,在初始化完3个classLoader以后,接下来首先通过catalinaLoader加载了org.apache.catalina.startup.Catalinal类,然后通过放射调用了org.apache.catalina.startup.Catalina#setParentClassLoader,具体代码片段如下:

    Class<?> startupClass =
        catalinaLoader.loadClass
        ("org.apache.catalina.startup.Catalina");
    Object startupInstance = startupClass.newInstance();
    
    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的setParentClassLoader放,那么到这里我们可能又要想知道,设置了parentClassLoader以后,sharedLoader又是在哪里使用的呢?这就需要我们接着来分析容器启动的代码。我们通过查看org.apache.catalina.startup.Catalina#getParentClassLoader调用栈,我们看到在StandardContext的startInternal方法中调用了它,那么我们查看一下它的代码,包含了如下代码片段:

    if (getLoader() == null) {
                WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
                webappLoader.setDelegate(getDelegate());
                setLoader(webappLoader);
    }
    try {
    
        if (ok) {
    
            // Start our subordinate components, if any
            if ((loader != null) && (loader instanceof Lifecycle))
                ((Lifecycle) loader).start();
            //other code    
        }
    catch(Exception e){
    }

    通过查看上面的代码,我们看到在StandardContext启动的时候,会创建webapploader,创建webapploader的时候会将getParentClassLoader方法返回的结果(这里返回的其实就是sharedLoader)赋值给自己的parentClassLoader变量,接着又会调用到Webapploader的start方法,因为WebappLoader符合Tomcat组件生命周期管理的模板方法模式,因此会调用到它的startInternal方法。我们接下来就来看看WebappLoader的startInternal,我们摘取一部分与本篇相关的代码片段如下:

    classLoader = createClassLoader();
    classLoader.setResources(container.getResources());
    classLoader.setDelegate(this.delegate);
    classLoader.setSearchExternalFirst(searchExternalFirst);

    从上的代码可以看到调用了createClassLoader方法创建一个classLoader,那么我们再看来看看createClassLoader的代码:

    private WebappClassLoader createClassLoader()
        throws Exception {
    
        Class<?> clazz = Class.forName(loaderClass);
        WebappClassLoader classLoader = null;
    
        if (parentClassLoader == null) {
            parentClassLoader = container.getParentClassLoader();
        }
        Class<?>[] argTypes = { ClassLoader.class };
        Object[] args = { parentClassLoader };
        Constructor<?> constr = clazz.getConstructor(argTypes);
        classLoader = (WebappClassLoader) constr.newInstance(args);
    
        return classLoader;
    
    }

    在上面的代码里面,loaderClass是WebappLoader的实例变量,其值为org.apache.catalina.loader.WebappClassLoader,那么上面的代码其实就是通过反射调用了WebappClassLoader的构造函数,然后传递了sharedLoader作为其父亲加载器。

    代码阅读到这里,我们已经基本清楚了Tomcat中ClassLoader的总体结构,总结如下: 在Tomcat存在common,cataina,shared三个公共的classloader,默认情况下,这三个classloader其实是同一个,都是common classloader,而针对每个webapp,也就是context(对应代码中的StandardContext类),都有自己的WebappClassLoader来加载每个应用自己的类。上面的描述,我们可以通过下图形象化的描述:

    清楚了Tomcat总体的ClassLoader结构以后,咋们就来进一步来分析一下WebAppClassLoader的代码,我们知道Java的ClassLoader机制有parent-first的机制,而这种机制是在loadClass方法保证的,一般情况下,我们只需要重写findClass方法就好了,而对于WebAppClassLoader,通过查看源代码,我们发现loadClass和findClass方法都进行了重写,那么我们首先就来看看它的loadClass方法,它的代码如下:

      1 public synchronized Class<?> loadClass(String name, boolean resolve)
      2     throws ClassNotFoundException {
      3 
      4     if (log.isDebugEnabled())
      5         log.debug("loadClass(" + name + ", " + resolve + ")");
      6     Class<?> clazz = null;
      7 
      8     // Log access to stopped classloader
      9     if (!started) {
     10         try {
     11             throw new IllegalStateException();
     12         } catch (IllegalStateException e) {
     13             log.info(sm.getString("webappClassLoader.stopped", name), e);
     14         }
     15     }
     16 
     17     // (0) Check our previously loaded local class cache
     18     // 1 
     19     clazz = findLoadedClass0(name);
     20     if (clazz != null) {
     21         if (log.isDebugEnabled())
     22             log.debug("  Returning class from cache");
     23         if (resolve)
     24             resolveClass(clazz);
     25         return (clazz);
     26     }
     27 
     28     // (0.1) Check our previously loaded class cache
     29     // 2
     30     clazz = findLoadedClass(name);
     31     if (clazz != null) {
     32         if (log.isDebugEnabled())
     33             log.debug("  Returning class from cache");
     34         if (resolve)
     35             resolveClass(clazz);
     36         return (clazz);
     37     }
     38 
     39     // (0.2) Try loading the class with the system class loader, to prevent
     40     //       the webapp from overriding J2SE classes
     41     // 3 
     42     try {
     43         clazz = system.loadClass(name);
     44         if (clazz != null) {
     45             if (resolve)
     46                 resolveClass(clazz);
     47             return (clazz);
     48         }
     49     } catch (ClassNotFoundException e) {
     50         // Ignore
     51     }
     52 
     53     // (0.5) Permission to access this class when using a SecurityManager
     54     if (securityManager != null) {
     55         int i = name.lastIndexOf('.');
     56         if (i >= 0) {
     57             try {
     58                 securityManager.checkPackageAccess(name.substring(0,i));
     59             } catch (SecurityException se) {
     60                 String error = "Security Violation, attempt to use " +
     61                     "Restricted Class: " + name;
     62                 log.info(error, se);
     63                 throw new ClassNotFoundException(error, se);
     64             }
     65         }
     66     }
     67 
     68     //4 
     69     boolean delegateLoad = delegate || filter(name);
     70 
     71     // (1) Delegate to our parent if requested
     72     // 5 
     73     if (delegateLoad) {
     74         if (log.isDebugEnabled())
     75             log.debug("  Delegating to parent classloader1 " + parent);
     76         ClassLoader loader = parent;
     77         if (loader == null)
     78             loader = system;
     79         try {
     80             clazz = Class.forName(name, false, loader);
     81             if (clazz != null) {
     82                 if (log.isDebugEnabled())
     83                     log.debug("  Loading class from parent");
     84                 if (resolve)
     85                     resolveClass(clazz);
     86                 return (clazz);
     87             }
     88         } catch (ClassNotFoundException e) {
     89             // Ignore
     90         }
     91     }
     92 
     93     // (2) Search local repositories
     94     if (log.isDebugEnabled())
     95         log.debug("  Searching local repositories");
     96     // 6 
     97     try {
     98         clazz = findClass(name);
     99         if (clazz != null) {
    100             if (log.isDebugEnabled())
    101                 log.debug("  Loading class from local repository");
    102             if (resolve)
    103                 resolveClass(clazz);
    104             return (clazz);
    105         }
    106     } catch (ClassNotFoundException e) {
    107         // Ignore
    108     }
    109 
    110     // (3) Delegate to parent unconditionally
    111     // 7
    112     if (!delegateLoad) {
    113         if (log.isDebugEnabled())
    114             log.debug("  Delegating to parent classloader at end: " + parent);
    115         ClassLoader loader = parent;
    116         if (loader == null)
    117             loader = system;
    118         try {
    119             clazz = Class.forName(name, false, loader);
    120             if (clazz != null) {
    121                 if (log.isDebugEnabled())
    122                     log.debug("  Loading class from parent");
    123                 if (resolve)
    124                     resolveClass(clazz);
    125                 return (clazz);
    126             }
    127         } catch (ClassNotFoundException e) {
    128             // Ignore
    129         }
    130     }
    131 
    132     throw new ClassNotFoundException(name);
    133 
    134 }

    我们一步步的来分析一下上面的代码做了什么事情。

    1. 标注1(第18行)代码,首先从当前ClassLoader的本地缓存中加载类,如果找到则返回。
    2. 标注2(第29行)代码,在本地缓存没有的情况下,调用ClassLoader的findLoadedClass方法查看jvm是否已经加载过此类,如果已经加载则直接返回。
    3. 标注3(第41行)代码,通过系统的来加载器加载此类,这里防止应用写的类覆盖了J2SE的类,这句代码非常关键,如果不写的话,就会造成你自己写的类有可能会把J2SE的类给替换调,另外假如你写了一个javax.servlet.Servlet类,放在当前应用的WEB-INF/class中,如果没有此句代码的保证,那么你自己写的类就会替换到Tomcat容器Lib中包含的类。
    4. 标注4(第68行)代码,判断是否需要委托给父类加载器进行加载,delegate属性默认为false,那么delegatedLoad的值就取决于filter的返回值了,filter方法中根据包名来判断是否需要进行委托加载,默认情况下会返回false.因此delegatedLoad为false
    5. 标注5(第72行)代码,因为delegatedLoad为false,那么此时不会委托父加载器去加载,这里其实是没有遵循parent-first的加载机制。
    6. 标注6(第96行)调用findClass方法在webapp级别进行加载
    7. 标注7(第111行)如果还是没有加载到类,并且不采用委托机制的话,则通过父类加载器去加载。

    通过上面的描述,我们可以知道Tomcat在加载webapp级别的类的时候,默认是不遵守parent-first的,这样做的好处是更好的实现了应用的隔离,但是坏处就是加大了内存浪费,同样的类库要在不同的app中都要加载一份。

    上面分析完了loadClass,我们接着在来分析一下findClass,通过分析findClass的代码,最终会调用org.apache.catalina.loader.WebappClassLoader#findClassInternal方法,那我们就来分析一下它的代码:

     1 protected Class<?> findClassInternal(String name)
     2     throws ClassNotFoundException {
     3 
     4     //
     5     if (!validate(name))
     6         throw new ClassNotFoundException(name);
     7 
     8     String tempPath = name.replace('.', '/');
     9     String classPath = tempPath + ".class";
    10 
    11     ResourceEntry entry = null;
    12 
    13     if (securityManager != null) {
    14         PrivilegedAction<ResourceEntry> dp =
    15             new PrivilegedFindResourceByName(name, classPath);
    16         entry = AccessController.doPrivileged(dp);
    17     } else {
    18         // 1 
    19         entry = findResourceInternal(name, classPath);
    20     }
    21 
    22     if (entry == null)
    23         throw new ClassNotFoundException(name);
    24 
    25     Class<?> clazz = entry.loadedClass;
    26     if (clazz != null)
    27         return clazz;
    28 
    29     synchronized (this) {
    30         clazz = entry.loadedClass;
    31         if (clazz != null)
    32             return clazz;
    33 
    34         if (entry.binaryContent == null)
    35             throw new ClassNotFoundException(name);
    36 
    37         try {
    38             // 2
    39             clazz = defineClass(name, entry.binaryContent, 0,
    40                     entry.binaryContent.length,
    41                     new CodeSource(entry.codeBase, entry.certificates));
    42         } catch (UnsupportedClassVersionError ucve) {
    43             throw new UnsupportedClassVersionError(
    44                     ucve.getLocalizedMessage() + " " +
    45                     sm.getString("webappClassLoader.wrongVersion",
    46                             name));
    47         }
    48         entry.loadedClass = clazz;
    49         entry.binaryContent = null;
    50         entry.source = null;
    51         entry.codeBase = null;
    52         entry.manifest = null;
    53         entry.certificates = null;
    54     }
    55 
    56     return clazz;
    57 
    58 }

    上面的代码标注1(第19行)的地方通过名称去当前webappClassLoader的仓库中查找对应的类文件,标注2(第38行)的代码,将找到的类文件通过defineClass转变为Jvm可以识别的Class对象返回。

    Reference

    Tomcat类加载器机制(Tomcat源代码阅读系列之六)

    Tomcat学习之ClassLoader

    tomcat的classloader机制

  • 相关阅读:
    Spring boot 使用多个RedisTemplate
    Spring boot 连接Redis实现HMSET操作
    Spring boot 工具类静态属性注入及多环境配置
    向量空间模型(Vector Space Model)的理解
    双数组Trie树中叶子结点check[t]=t的证明
    谈谈我对隐马尔可夫模型的理解
    Information Retrieval 倒排索引 学习笔记
    朴素贝叶斯文本分类简单介绍
    Python Thrift 简单示例
    迭代器模式(Iterator)
  • 原文地址:https://www.cnblogs.com/549294286/p/3711712.html
Copyright © 2020-2023  润新知