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中做了如下修改:

rem Add on extra jar file to CLASSPATH
rem Note that there are no quotes as we do not want to introduce random
rem quotes into the CLASSPATH
if "%CLASSPATH%" == "" goto emptyClasspath
set "CLASSPATH=%CLASSPATH%;"
:emptyClasspath
set "CLASSPATH=%CLASSPATH%%CATALINA_HOME%\bin\bootstrap.jar"
rem Add tomcat-juli.jar to classpath
rem tomcat-juli.jar can be over-ridden per instance
if not exist "%CATALINA_BASE%\bin\tomcat-juli.jar" goto juliClasspathHome
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個類裝載器:
public void init() throws Exception{
	// Set Catalina path
	setCatalinaHome();
	setCatalinaBase();

	initClassLoaders();

	Thread.currentThread().setContextClassLoader(catalinaLoader);

	SecurityClassLoad.securityClassLoad(catalinaLoader);
	...
}	

private void initClassLoaders() {
	try {
		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 = createClassLoader("server", commonLoader);
		sharedLoader = createClassLoader("shared", commonLoader);
	} catch (Throwable t) {
		handleThrowable(t);
		log.error("Class loader creation threw exception", t);
		System.exit(1);
	}
}

Common Class Loader

首先創建CommonLoader,是在createClassLoader方法中完成的,代碼如下:
private ClassLoader createClassLoader(String name, ClassLoader parent)throws Exception {
	String value = CatalinaProperties.getProperty(name + ".loader");
	if ((value == null) || (value.equals("")))
		return parent;

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

	ClassLoader classLoader = ClassLoaderFactory.createClassLoader(repositories, parent);
	...

	return classLoader;
}
(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的配置項:
common.loader=${catalina.base}/lib,${catalina.base}/lib/*.jar,${catalina.home}/lib,${catalina.home}/lib/*.jar
server.loader=
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方法:
 protected synchronized void startInternal() throws LifecycleException {
	...
        if (getLoader() == null) {
            WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
            webappLoader.setDelegate(getDelegate());
            setLoader(webappLoader);
        }
	...
        // Binding thread
        ClassLoader oldCCL = bindThread();
        try {

            if (ok) {
                
                // Start our subordinate components, if any
                if ((loader != null) && (loader instanceof Lifecycle))
                    ((Lifecycle) loader).start();

                // since the loader just started, the webapp classloader is now
                // created.
                // By calling unbindThread and bindThread in a row, we setup the
                // current Thread CCL to be the webapp classloader
                unbindThread(oldCCL);
                oldCCL = bindThread();
	}
	...
        } finally {
            // Unbinding thread
            unbindThread(oldCCL);
        }
}
很明顯,context在啓動的時候創建了一個loader組件,webapploader正是loader的實現類,這個類並不是最終的類裝載器,在這個類裏面有一個webappclassloader類型的字段叫classloader,這個classloader的創建是在loader組件的start方法中完成的
    protected void startInternal() throws LifecycleException {
		...
        // Construct a class loader based on our current repositories list
        try {
            classLoader = createClassLoader();
            classLoader.setResources(container.getResources());
            classLoader.setDelegate(this.delegate);
            classLoader.setSearchExternalFirst(searchExternalFirst);
            if (container instanceof StandardContext) {
                classLoader.setAntiJARLocking(
                        ((StandardContext) container).getAntiJARLocking());
                classLoader.setClearReferencesStatic(
                        ((StandardContext) container).getClearReferencesStatic());
                classLoader.setClearReferencesStopThreads(
                        ((StandardContext) container).getClearReferencesStopThreads());
                classLoader.setClearReferencesStopTimerThreads(
                        ((StandardContext) container).getClearReferencesStopTimerThreads());
                classLoader.setClearReferencesHttpClientKeepAliveThread(
                        ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
            }

            for (int i = 0; i < repositories.length; i++) {
                classLoader.addRepository(repositories[i]);
            }

            // Configure our repositories
            setRepositories();
            setClassPath();

            setPermissions();

            ((Lifecycle) classLoader).start();
			...
        } catch (Throwable t) {
         ...
        }
		...
    }
(1)、在createClassLoader方法中通過反射實例化了org.apache.catalina.loader.WebappClassLoader這個類,並調用了它的setParentClassLoader設置其父裝載器爲standardClassLoader
    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;

    }
(2)、爲webappclassloader添加倉庫(倉庫表示類裝載器會在哪些路徑搜索類)
將/WEB-INF/classes目錄添加到倉庫中,然後將/WEB-INF/lib目錄下的jar包也添加到倉庫中
private void setRepositories() throws IOException {
	...
	// Setting up the class repository (/WEB-INF/classes), if it exists
	String classesPath = "/WEB-INF/classes";
	
	...
	// Adding the repository to the class loader
	classLoader.addRepository(classesPath + "/", classRepository);

	// Setting up the JAR repository (/WEB-INF/lib), if it exists
	String libPath = "/WEB-INF/lib";
	...
	// Looking up directory /WEB-INF/lib in the context
	NamingEnumeration<NameClassPair> enumeration = libDir.list("");
	while (enumeration.hasMoreElements()) {
		NameClassPair ncPair = enumeration.nextElement();
		String filename = libPath + "/" + ncPair.getName();
		if (!filename.endsWith(".jar"))
			continue;
		...
		try {
			JarFile jarFile = new JarFile(destFile);
			classLoader.addJar(filename, jarFile, destFile);
		} catch (Exception ex) {
		 ...
		}
		...
	}
}
(3)、爲類裝載器設置權限,這裏Globals.IS_SECURITY_ENABLED值爲false,表示安全機制未打開,直接返回
(4)、啓動這個loader
Webappclassloader設計的過程中考慮了優化和安全兩方面。例如,它會緩存之前已經載入的類以提高性能。此外,它還會緩存失敗的類的名字,下次再次請求加載相同的類時直接拋出ClassNotFoundException異常。考慮到安全性,不允許載入指定的某些類,這些類在triggers數組中,目前有兩個類:
    protected static final String[] triggers = {
        "javax.servlet.Servlet", "javax.el.Expression"       // Servlet API
    };

WebappClassLoader裝載類

loadClass是在其loadClass方法中完成的,下面詳細分析這個方法:
	public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
		...
		// 先檢查本地緩存
		clazz = findLoadedClass0(name);
		if (clazz != null) {
			if (log.isDebugEnabled())
				log.debug("  Returning class from cache");
			if (resolve)
				resolveClass(clazz);
			return (clazz);
		}

		// 如果本地緩存沒有,則檢查上一級緩存
		clazz = findLoadedClass(name);
		if (clazz != null) {
			if (log.isDebugEnabled())
				log.debug("  Returning class from cache");
			if (resolve)
				resolveClass(clazz);
			return (clazz);
		}

		// 如果兩個緩存都沒有,則使用系統的類裝載器進行裝載,防止Web應用程序中的類覆蓋J2EE的類
		try {
			clazz = system.loadClass(name);
			if (clazz != null) {
				if (resolve)
					resolveClass(clazz);
				return (clazz);
			}
		} catch (ClassNotFoundException e) {
			// Ignore
		}

		// 如果啓用了SecurityManager,則檢查此類是否允許被載入,如果不允許,則拋出異常
		if (securityManager != null) {
			int i = name.lastIndexOf('.');
			if (i >= 0) {
				try {
					securityManager.checkPackageAccess(name.substring(0, i));
				} catch (SecurityException se) {
					String error = "Security Violation, attempt to use " + "Restricted Class: " + name;
					log.info(error, se);
					throw new ClassNotFoundException(error, se);
				}
			}
		}

		boolean delegateLoad = delegate || filter(name);

		// 若打開了delegateLoad標誌位,調用父裝載器來加載。如果父裝載器爲null,使用系統類裝載器裝載
		if (delegateLoad) {
			if (log.isDebugEnabled())
				log.debug("  Delegating to parent classloader1 " + parent);
			ClassLoader loader = parent;
			if (loader == null)
				loader = system;
			try {
				clazz = Class.forName(name, false, loader);
				if (clazz != null) {
					if (log.isDebugEnabled())
						log.debug("  Loading class from parent");
					if (resolve)
						resolveClass(clazz);
					return (clazz);
				}
			} catch (ClassNotFoundException e) {
				// Ignore
			}
		}

		// 從本地倉庫中載入相關類
		if (log.isDebugEnabled())
			log.debug("  Searching local repositories");
		try {
			clazz = findClass(name);
			if (clazz != null) {
				if (log.isDebugEnabled())
					log.debug("  Loading class from local repository");
				if (resolve)
					resolveClass(clazz);
				return (clazz);
			}
		} catch (ClassNotFoundException e) {
			// Ignore
		}

		// 若當前倉庫中沒有需要的類,且delegateLoad標誌位關閉,則使用父裝載器。若父裝載器爲null,使用系統類裝載器來裝載
		if (!delegateLoad) {
			if (log.isDebugEnabled())
				log.debug("  Delegating to parent classloader at end: " + parent);
			ClassLoader loader = parent;
			if (loader == null)
				loader = system;
			try {
				clazz = Class.forName(name, false, loader);
				if (clazz != null) {
					if (log.isDebugEnabled())
						log.debug("  Loading class from parent");
					if (resolve)
						resolveClass(clazz);
					return (clazz);
				}
			} catch (ClassNotFoundException e) {
				// Ignore
			}
		}
		//仍未找到,拋出異常
		throw new ClassNotFoundException(name);

	}
整個思路是:先到緩存中獲取,如果緩存中有直接返回,否則根據delegateLoad採取不同的加載方式。如果未啓用這個標誌:先本地倉庫加載再父裝載器或者系統類裝載器裝載;如果啓用了這個標誌:直接由父裝載器或者系統類裝載器裝載。

類緩存

tomcat之所以採用自定義類裝載器,除了不同應用之間有相同類不好解決之外,還有一個原因是可以緩存類以提高速度。每個由webappclassloader裝載的類被視爲資源,用ResourceEntry表示。加入緩存的代碼是在loadclass方法中完成的,前面提到會搜索本地倉庫,就是在這步調用了findClass方法完成了類的查找,並把找到的類封裝成ResourceEntry,最後把這個resourceEntry放入resourceEntries中緩存起來。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章