首页 > 代码库 > Tomcat学习之ClassLoader

Tomcat学习之ClassLoader

 

Tomcat学习之ClassLoader

技术分享 分类:
 

目录(?)[+]

 

类装载器

JDK中提供了3种不同的类加载器:启动类装载器,扩展类装载器和系统类装载器。引导类装载器,用于引导启动Java虚拟机,当执行一个JAVA程序时,就会启动引导类装载器,它是使用本地代码来实现的,会装载%JAVA_HOME%\\jre\lib\rt.jar,它是所有类装载器类的父装载器。扩展类装载器负责载入标准扩展目录中的类,其搜索路径是%JAVA_HOME%\jre\lib\ext,只需要将打包好的jar文件放入这个目录就可以了,给开发提供了很大的便利性。系统类装载器是默认的装载器,其搜索路径是classpath。

JVM到底使用的是哪一个类装载器,取决于类装载器的代理模式。每当需要装载一个类的时候,会首先调用系统类装载器,但是系统类装载器并不会立即装载,而是将其交给父装载器:扩展类装载器,扩展类装载器其将交给引导类装载器,引导类装载器没有父装载器,它会尝试装载这个类,如果找不到这个类,会交给扩展类装载器,如果扩展类装载器还是没有找到,会交给系统类装载器,如果系统类装载器还是没有找到这个类,则会抛出java.lang.ClassNotFoundException异常。代理模式主要是为了解决类装载的安全问题。例如:对于自定类的java.lang.Object类,永远得不到装载,除非,rt.jar中确实没有这个类。

tomcat也提供了几种不同的类装载器用于加载不同位置的jar包和class文件,特别是Context容器需要有一个单独的类装载器,因为不同应用可能有相同的类,如果用同一个类装载器去装载,就不知道该加载哪个应用里面的类了。这些类装载器之间的关系如下图所示:

技术分享

系统类装载器

tomcat的系统类装载器和JDK的系统类装载器有点不同的地方是搜索路径并不相同,在catalina.bat中做了如下修改:

[plain] view plain copy
 
 print?
  1. rem Add on extra jar file to CLASSPATH  
  2. rem Note that there are no quotes as we do not want to introduce random  
  3. rem quotes into the CLASSPATH  
  4. if "%CLASSPATH%" == "" goto emptyClasspath  
  5. set "CLASSPATH=%CLASSPATH%;"  
  6. :emptyClasspath  
  7. set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar"  
  8. rem Add tomcat-juli.jar to classpath  
  9. rem tomcat-juli.jar can be over-ridden per instance  
  10. if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome  
  11. set "CLASSPATH=%CLASSPATH%;%CATALINA_BASE%\bin\tomcat-juli.jar"  
先将classpath清空,因为classpath中可能有tomcat启动相关的类会影响tomcat的正常启动。然后将bootstrap.jar和tomcat-juli.jar加入classpath中,在catalina.bat中调用了Bootstrap类的main方法,这里系统类装载器会装载Bootstrap类,Bootstrap类用到的Catalina类也是由系统类装载器装载的。
随后在Bootstrap的init方法中创建了3个类装载器:
[java] view plain copy
 
 print?
  1. public void init() throws Exception{  
  2.     // Set Catalina path  
  3.     setCatalinaHome();  
  4.     setCatalinaBase();  
  5.   
  6.     initClassLoaders();  
  7.   
  8.     Thread.currentThread().setContextClassLoader(catalinaLoader);  
  9.   
  10.     SecurityClassLoad.securityClassLoad(catalinaLoader);  
  11.     ...  
  12. }     
  13.   
  14. private void initClassLoaders() {  
  15.     try {  
  16.         commonLoader = createClassLoader("common", null);  
  17.         if( commonLoader == null ) {  
  18.             // no config file, default to this loader - we might be in a ‘single‘ env.  
  19.             commonLoader=this.getClass().getClassLoader();  
  20.         }  
  21.         catalinaLoader = createClassLoader("server", commonLoader);  
  22.         sharedLoader = createClassLoader("shared", commonLoader);  
  23.     } catch (Throwable t) {  
  24.         handleThrowable(t);  
  25.         log.error("Class loader creation threw exception", t);  
  26.         System.exit(1);  
  27.     }  
  28. }  

Common Class Loader

首先创建CommonLoader,是在createClassLoader方法中完成的,代码如下:
[java] view plain copy
 
 print?
  1. private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {  
  2.     String value = CatalinaProperties.getProperty(name + ".loader");  
  3.     if ((value == null) || (value.equals("")))  
  4.         return parent;  
  5.   
  6.     value = replace(value);  
  7.   
  8.     List<Repository> repositories = new ArrayList<Repository>();  
  9.     StringTokenizer tokenizer = new StringTokenizer(value, ",");  
  10.     while (tokenizer.hasMoreElements()) {  
  11.         String repository = tokenizer.nextToken().trim();  
  12.         if (repository.length() == 0) {  
  13.             continue;  
  14.         }  
  15.   
  16.         // Check for a JAR URL repository  
  17.         try {  
  18.             @SuppressWarnings("unused")  
  19.             URL url = new URL(repository);  
  20.             repositories.add(new Repository(repository, RepositoryType.URL));  
  21.             continue;  
  22.         } catch (MalformedURLException e) {  
  23.             // Ignore  
  24.         }  
  25.   
  26.         // Local repository  
  27.         if (repository.endsWith("*.jar")) {  
  28.             repository = repository.substring(0, repository.length() - "*.jar".length());  
  29.             repositories.add(new Repository(repository, RepositoryType.GLOB));  
  30.         } else if (repository.endsWith(".jar")) {  
  31.             repositories.add(new Repository(repository, RepositoryType.JAR));  
  32.         } else {  
  33.             repositories.add(new Repository(repository, RepositoryType.DIR));  
  34.         }  
  35.     }  
  36.   
  37.     ClassLoader classLoader = ClassLoaderFactory.createClassLoader(repositories, parent);  
  38.     ...  
  39.   
  40.     return classLoader;  
  41. }  
(1)、从bootstrap.jar包可以找到catalina.properties文件:common.loader=${catalina.home}/lib,${catalina.home}/lib/*.jar,很明显这个类装载器搜索路径就是${catalina.home}/lib和${catalina.home}/lib/*.jar,之所以叫common class loader,是因为它加载每个应用要用到的公共jar包和class文件;
(2)、遍历values,将每个value封装成Repository,然后根据repository和parent创建这个classLoader,在这个方法中parent传入的是null值,代表这个类装载器的父装载器是系统类装载器,实际上返回的是StandardClassLoader类,StandardClassLoader类是URLClassLoader的子类,即将被废弃。之所以返回的是StandardClassLoader是在ClassLoaderFactory的createClassLoader方法中被包装了一层。

Catalina Class Loader

common class loader创建好之后,又创建了catalinaLoader,其搜索路径为空,以下是catalina.properties的配置项:
[java] view plain copy
 
 print?
  1. common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar  
  2. server.loader=  
  3. shared.loader=  
common class loader是以common class loader为父装载器的,因此其搜索路径和common class loader一样。catalina class loader创建好后,在init方法中随即调用了Thread.currentThread().setContextClassLoader(catalinaLoader);将其设置为当前线程的类装载器

Shared Class Loader

已不再使用,tomcat早期的版本在使用这个类装载器,负责装载应用中公用的类,后来这些公用的类被移到了{catalina.base}/lib目录下,这个装载器暂时未被使用
综上所述:tomcat在启动的时候初始化了三个类加载器,commonLoader,catalinaLoader,sharedLoader.其中commonLoader是另外两个的父装载器,且为standardClassLoader类型,tomcat真正使用的是commonLoader,engine,host,connector等都是使用commonLoader装载的。
 

Webapp Class Loader

这个类装载器是tomcat自定义的类装载器,先来看看类图:
技术分享
tomcat的一个service除了容器和连接器外还有很多组件,比如sessionManager,logger,loader等,这个类装载器是以组件的形式附着在每个容器上的,Engine和Host的这两个容器的loader组件为null,context里面是有值的,看看context的startInternal方法:
[java] view plain copy
 
 print?
  1. protected synchronized void startInternal() throws LifecycleException {  
  2. ...  
  3.        if (getLoader() == null) {  
  4.            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());  
  5.            webappLoader.setDelegate(getDelegate());  
  6.            setLoader(webappLoader);  
  7.        }  
  8. ...  
  9.        // Binding thread  
  10.        ClassLoader oldCCL = bindThread();  
  11.        try {  
  12.   
  13.            if (ok) {  
  14.                  
  15.                // Start our subordinate components, if any  
  16.                if ((loader != null) && (loader instanceof Lifecycle))  
  17.                    ((Lifecycle) loader).start();  
  18.   
  19.                // since the loader just started, the webapp classloader is now  
  20.                // created.  
  21.                // By calling unbindThread and bindThread in a row, we setup the  
  22.                // current Thread CCL to be the webapp classloader  
  23.                unbindThread(oldCCL);  
  24.                oldCCL = bindThread();  
  25. }  
  26. ...  
  27.        } finally {  
  28.            // Unbinding thread  
  29.            unbindThread(oldCCL);  
  30.        }  
很明显,context在启动的时候创建了一个loader组件,webapploader正是loader的实现类,这个类并不是最终的类装载器,在这个类里面有一个webappclassloader类型的字段叫classloader,这个classloader的创建是在loader组件的start方法中完成的
[java] view plain copy
 
 print?
  1.   protected void startInternal() throws LifecycleException {  
  2. ...  
  3.       // Construct a class loader based on our current repositories list  
  4.       try {  
  5.           classLoader = createClassLoader();  
  6.           classLoader.setResources(container.getResources());  
  7.           classLoader.setDelegate(this.delegate);  
  8.           classLoader.setSearchExternalFirst(searchExternalFirst);  
  9.           if (container instanceof StandardContext) {  
  10.               classLoader.setAntiJARLocking(  
  11.                       ((StandardContext) container).getAntiJARLocking());  
  12.               classLoader.setClearReferencesStatic(  
  13.                       ((StandardContext) container).getClearReferencesStatic());  
  14.               classLoader.setClearReferencesStopThreads(  
  15.                       ((StandardContext) container).getClearReferencesStopThreads());  
  16.               classLoader.setClearReferencesStopTimerThreads(  
  17.                       ((StandardContext) container).getClearReferencesStopTimerThreads());  
  18.               classLoader.setClearReferencesHttpClientKeepAliveThread(  
  19.                       ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());  
  20.           }  
  21.   
  22.           for (int i = 0; i < repositories.length; i++) {  
  23.               classLoader.addRepository(repositories[i]);  
  24.           }  
  25.   
  26.           // Configure our repositories  
  27.           setRepositories();  
  28.           setClassPath();  
  29.   
  30.           setPermissions();  
  31.   
  32.           ((Lifecycle) classLoader).start();  
  33.     ...  
  34.       } catch (Throwable t) {  
  35.        ...  
  36.       }  
  37. ...  
  38.   }  
(1)、在createClassLoader方法中通过反射实例化了org.apache.catalina.loader.WebappClassLoader这个类,并调用了它的setParentClassLoader设置其父装载器为standardClassLoader
[java] view plain copy
 
 print?
  1. private WebappClassLoader createClassLoader()  
  2.     throws Exception {  
  3.   
  4.     Class<?> clazz = Class.forName(loaderClass);  
  5.     WebappClassLoader classLoader = null;  
  6.   
  7.     if (parentClassLoader == null) {  
  8.         parentClassLoader = container.getParentClassLoader();  
  9.     }  
  10.     Class<?>[] argTypes = { ClassLoader.class };  
  11.     Object[] args = { parentClassLoader };  
  12.     Constructor<?> constr = clazz.getConstructor(argTypes);  
  13.     classLoader = (WebappClassLoader) constr.newInstance(args);  
  14.   
  15.     return classLoader;  
  16.   
  17. }  
(2)、为webappclassloader添加仓库(仓库表示类装载器会在哪些路径搜索类)
将/WEB-INF/classes目录添加到仓库中,然后将/WEB-INF/lib目录下的jar包也添加到仓库中
[java] view plain copy
 
 print?
  1. private void setRepositories() throws IOException {  
  2.     ...  
  3.     // Setting up the class repository (/WEB-INF/classes), if it exists  
  4.     String classesPath = "/WEB-INF/classes";  
  5.       
  6.     ...  
  7.     // Adding the repository to the class loader  
  8.     classLoader.addRepository(classesPath + "/", classRepository);  
  9.   
  10.     // Setting up the JAR repository (/WEB-INF/lib), if it exists  
  11.     String libPath = "/WEB-INF/lib";  
  12.     ...  
  13.     // Looking up directory /WEB-INF/lib in the context  
  14.     NamingEnumeration<NameClassPair> enumeration = libDir.list("");  
  15.     while (enumeration.hasMoreElements()) {  
  16.         NameClassPair ncPair = enumeration.nextElement();  
  17.         String filename = libPath + "/" + ncPair.getName();  
  18.         if (!filename.endsWith(".jar"))  
  19.             continue;  
  20.         ...  
  21.         try {  
  22.             JarFile jarFile = new JarFile(destFile);  
  23.             classLoader.addJar(filename, jarFile, destFile);  
  24.         } catch (Exception ex) {  
  25.          ...  
  26.         }  
  27.         ...  
  28.     }  
  29. }  
(3)、为类装载器设置权限,这里Globals.IS_SECURITY_ENABLED值为false,表示安全机制未打开,直接返回
(4)、启动这个loader
Webappclassloader设计的过程中考虑了优化和安全两方面。例如,它会缓存之前已经载入的类以提高性能。此外,它还会缓存失败的类的名字,下次再次请求加载相同的类时直接抛出ClassNotFoundException异常。考虑到安全性,不允许载入指定的某些类,这些类在triggers数组中,目前有两个类:
[java] view plain copy
 
 print?
  1. protected static final String[] triggers = {  
  2.     "javax.servlet.Servlet", "javax.el.Expression"       // Servlet API  
  3. };  

WebappClassLoader装载类

loadClass是在其loadClass方法中完成的,下面详细分析这个方法:
[java] view plain copy
 
 print?
  1. public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {  
  2.     ...  
  3.     // 先检查本地缓存  
  4.     clazz = findLoadedClass0(name);  
  5.     if (clazz != null) {  
  6.         if (log.isDebugEnabled())  
  7.             log.debug("  Returning class from cache");  
  8.         if (resolve)  
  9.             resolveClass(clazz);  
  10.         return (clazz);  
  11.     }  
  12.   
  13.     // 如果本地缓存没有,则检查上一级缓存  
  14.     clazz = findLoadedClass(name);  
  15.     if (clazz != null) {  
  16.         if (log.isDebugEnabled())  
  17.             log.debug("  Returning class from cache");  
  18.         if (resolve)  
  19.             resolveClass(clazz);  
  20.         return (clazz);  
  21.     }  
  22.   
  23.     // 如果两个缓存都没有,则使用系统的类装载器进行装载,防止Web应用程序中的类覆盖J2EE的类  
  24.     try {  
  25.         clazz = system.loadClass(name);  
  26.         if (clazz != null) {  
  27.             if (resolve)  
  28.                 resolveClass(clazz);  
  29.             return (clazz);  
  30.         }  
  31.     } catch (ClassNotFoundException e) {  
  32.         // Ignore  
  33.     }  
  34.   
  35.     // 如果启用了SecurityManager,则检查此类是否允许被载入,如果不允许,则抛出异常  
  36.     if (securityManager != null) {  
  37.         int i = name.lastIndexOf(‘.‘);  
  38.         if (i >= 0) {  
  39.             try {  
  40.                 securityManager.checkPackageAccess(name.substring(0, i));  
  41.             } catch (SecurityException se) {  
  42.                 String error = "Security Violation, attempt to use " + "Restricted Class: " + name;  
  43.                 log.info(error, se);  
  44.                 throw new ClassNotFoundException(error, se);  
  45.             }  
  46.         }  
  47.     }  
  48.   
  49.     boolean delegateLoad = delegate || filter(name);  
  50.   
  51.     // 若打开了delegateLoad标志位,调用父装载器来加载。如果父装载器为null,使用系统类装载器装载  
  52.     if (delegateLoad) {  
  53.         if (log.isDebugEnabled())  
  54.             log.debug("  Delegating to parent classloader1 " + parent);  
  55.         ClassLoader loader = parent;  
  56.         if (loader == null)  
  57.             loader = system;  
  58.         try {  
  59.             clazz = Class.forName(name, false, loader);  
  60.             if (clazz != null) {  
  61.                 if (log.isDebugEnabled())  
  62.                     log.debug("  Loading class from parent");  
  63.                 if (resolve)  
  64.                     resolveClass(clazz);  
  65.                 return (clazz);  
  66.             }  
  67.         } catch (ClassNotFoundException e) {  
  68.             // Ignore  
  69.         }  
  70.     }  
  71.   
  72.     // 从本地仓库中载入相关类  
  73.     if (log.isDebugEnabled())  
  74.         log.debug("  Searching local repositories");  
  75.     try {  
  76.         clazz = findClass(name);  
  77.         if (clazz != null) {  
  78.             if (log.isDebugEnabled())  
  79.                 log.debug("  Loading class from local repository");  
  80.             if (resolve)  
  81.                 resolveClass(clazz);  
  82.             return (clazz);  
  83.         }  
  84.     } catch (ClassNotFoundException e) {  
  85.         // Ignore  
  86.     }  
  87.   
  88.     // 若当前仓库中没有需要的类,且delegateLoad标志位关闭,则使用父装载器。若父装载器为null,使用系统类装载器来装载  
  89.     if (!delegateLoad) {  
  90.         if (log.isDebugEnabled())  
  91.             log.debug("  Delegating to parent classloader at end: " + parent);  
  92.         ClassLoader loader = parent;  
  93.         if (loader == null)  
  94.             loader = system;  
  95.         try {  
  96.             clazz = Class.forName(name, false, loader);  
  97.             if (clazz != null) {  
  98.                 if (log.isDebugEnabled())  
  99.                     log.debug("  Loading class from parent");  
  100.                 if (resolve)  
  101.                     resolveClass(clazz);  
  102.                 return (clazz);  
  103.             }  
  104.         } catch (ClassNotFoundException e) {  
  105.             // Ignore  
  106.         }  
  107.     }  
  108.     //仍未找到,抛出异常  
  109.     throw new ClassNotFoundException(name);  
  110.   
  111. }  
整个思路是:先到缓存中获取,如果缓存中有直接返回,否则根据delegateLoad采取不同的加载方式。如果未启用这个标志:先本地仓库加载再父装载器或者系统类装载器装载;如果启用了这个标志:直接由父装载器或者系统类装载器装载。

类缓存

tomcat之所以采用自定义类装载器,除了不同应用之间有相同类不好解决之外,还有一个原因是可以缓存类以提高速度。每个由webappclassloader装载的类被视为资源,用ResourceEntry表示。加入缓存的代码是在loadclass方法中完成的,前面提到会搜索本地仓库,就是在这步调用了findClass方法完成了类的查找,并把找到的类封装成ResourceEntry,最后把这个resourceEntry放入resourceEntries中缓存起来。

Tomcat学习之ClassLoader