【问题】
Tomcat
作为一个 Java Web
容器,他在启动时会加载其他用户的代码,而其他用户又可能依赖了其他的 jar
包,因此 Tomcat
是如何将所有的 class
文件加载到容器的呢?
【思路】
建议复习: JVM双亲委派机制与Tomcat
关于类加载器,其实在前面的双亲委派模型中已经简单讲过,但是具体的细节没有多讲,我们大体知道 Tomcat
有4种类加载器:
Common
类加载器,用于加载 Tomcat
和各个 Web
应用共享的类 Share
类加载器,用于加载各个 Web
共享的类 Server
类加载器,用于加载 Tomcat
各个类 WebApp
类加载器,用于加载各个 WebApp
中的类 而对于如何具体的加载各个 class
,对于 class
文件,直接 load
即可,对于文件夹,应该递归查找子文件夹和文件,对于 jar
包,同理应该解压并加载里面的 class
文件和 jar
包。
【Tomcat】
对于上面的理论,其实在 JDK
中都有现成的工具类: URLClassLoader
,因此在 Tomcat
的加载器中,很大一部分都是利用的这个类再加上一些业务规则。
对于 Tomcat
来说,各个类加载器的父子关系如下:
在 Tomcat
中,还可以通过配置是否委托来打破双亲委派机制:
对于 WebApp
加载器,如果按照双亲委派,那么在需要加载某个类的时候,会首先委派给 JVM
的加载器,如果没有加载成功则再委派给 Common
类加载器,接下来依次是 Shared
类加载器, WebApp
类加载器。
但是在 Tomcat
实现中默认并不是这样,而是首先交给 JVM
类加载器加载,如果 JVM
加载器没有加载成功,再从 WebApp
类中加载,如果还是没有加载成功,再委托给 Shared
和 Common
类加载器加载。这样便是违背了双亲委派机制。
为什么要这么实现呢?个人认为是 Tomcat
认为一般用户不会提供多余的类,并且在实际开发中 Share
和 Common
目录用的非常少,因此默认直接取 WebApp
先查找,这样速度更快。
【代码】
首先是三个父加载器: Common
, Server
, Shared
加载器:
**Bootstrap###initClassLoaders() **
这里就是启动 Tomcat
之前的初始化类加载器。
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); } }
可以看到,首先是初始化 commonLoader
,它的父加载器为 null
然后是 catalinaLoader
,它的父加载器是 commonLoader
最后是 sharedLoader
,它的父加载器同样是 commonLoader
private ClassLoader createClassLoader(String name, ClassLoader parent) throws Exception { //读取配置文件,获得对应的值 //例如: //common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar"... String value = CatalinaProperties.getProperty(name + ".loader"); if ((value == null) || (value.equals(""))) return parent; //替换本地变量的值,比如${catalina_home}换为真正的路径 value = replace(value); List <repository> repositories = new ArrayList<>(); //拆分为单个的路径 //这里的getPaths()方法是通过正则表达式匹配""或,拆分的, //为什么不使用String.split()? String[] repositoryPaths = getPaths(value); for (String repository : repositoryPaths) { // Check for a JAR URL repository //URL包 try { @SuppressWarnings("unused") URL url = new URL(repository); repositories.add(new Repository(repository, RepositoryType.URL)); continue; } catch (MalformedURLException e) { // Ignore } //*.jar包 // Local repository if (repository.endsWith("*.jar")) { repository = repository.substring (0, repository.length() - "*.jar".length()); repositories.add(new Repository(repository, RepositoryType.GLOB)); } //具体的jar包 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); } </repository>
//。。。 return AccessController.doPrivileged( new PrivilegedAction <urlclassloader> () { @Override public URLClassLoader run() { if (parent == null) return new URLClassLoader(array); else return new URLClassLoader(array, parent); } }); </urlclassloader>
这个方法比较长,前面还有挺多代码,大概是验证 URL
的有效性,这里先省略
这里可以看到,这三个类加载其实最底层都是 URLClassLoader
,关于 URLClassLoader
,我们以后再说,这里我们重点关注下 WebAppClassLoader
if (getLoader() == null) { //Webap WebappLoader webappLoader = new WebappLoader(getParentClassLoader()); webappLoader.setDelegate(getDelegate()); setLoader(webappLoader); }
这个是 StandardContext##startInternal()
方法的代码片段,可以看到, Context
包含一个属性: private Loader loader = null;
,而 loader
初始化就在 startInternal
这个 loader
有什么用呢?
在 DefaultInstanceManager
赋值 classLoader
的时候,便是使用的这个 Context
的 classLoader
classLoader = catalinaContext.getLoader().getClassLoader();
而 DefaultInstanceManager
正是用来加载 Servlet
的工具类。
说远了,这里一句话就是 WebAppClassLoader
便是用来加载 Servlet
的 ClassLoader
下面来看看 WebAppClassLoader
//WebappClassLoader public class WebappClassLoader extends WebappClassLoaderBase //WebappClassLoaderBase public abstract class WebappClassLoaderBase extends URLClassLoader implements Lifecycle, InstrumentableClassLoader, WebappProperties, PermissionCheck
可以发现, WebappClassLoader
继承自 WebappClassLoaderBase
,而 WebappClassLoaderBase
继承自 URLClassLoader
也就是说, WebappClassLoader
本质上还是 URLClassLoader
WebappClassLoader
中方法并不多,主要代码都在 WebappClassLoaderBase
中。
protected WebappClassLoaderBase(ClassLoader parent) { super(new URL[0], parent); //初始化父加载器属性 ClassLoader p = getParent(); if (p == null) { //如果父加载器为空,则设置为ApplicationClassLoader p = getSystemClassLoader(); } this.parent = p; //初始化JavaSe加载器 //String类的加载器应该是最顶层的类加载器: //默认应该是C++实现,因此这里一般都会返回null ClassLoader j = String.class.getClassLoader(); if (j == null) { j = getSystemClassLoader(); //如果String.class.getClassLoader返回null //则找到能引用的最高级别的类加载器 while (j.getParent() != null) { j = j.getParent(); } } this.javaseClassLoader = j; securityManager = System.getSecurityManager(); if (securityManager != null) { refreshPolicy(); } }
可以看到,这里主要就是初始化了两个类加载器属性
接下来比那时整个类的核心: loadClass()
@Override public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException { synchronized (getClassLoadingLock(name)) { Class<?> clazz = null; // (0) Check our previously loaded local class cache //首先查找本地缓存是否已经加载过了 clazz = findLoadedClass0(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } //如果没有加载成功,则看看JVM是否加载过 clazz = findLoadedClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } //然后尝试使用JVM ClassLoader加载此类 //这一步是必须的 String resourceName = binaryNameToPath(name, false); ClassLoader javaseLoader = getJavaseClassLoader(); boolean tryLoadingFromJavaseLoader; tryLoadingFromJavaseLoader = (javaseLoader.getResource(resourceName) != null); //如果能够加载,则加载此类 if (tryLoadingFromJavaseLoader) { try { clazz = javaseLoader.loadClass(name); if (clazz != null) { if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } //否则 //1. 检查用户是否配置需要委托,默认fasle boolean delegateLoad = delegate || filter(name, true); // 如果需要委托的话 if (delegateLoad) { try { //使用父加载器加载 clazz = Class.forName(name, false, parent); if (clazz != null) { if (log.isDebugEnabled()) log.debug(" Loading class from parent"); if (resolve) resolveClass(clazz); return clazz; } } catch (ClassNotFoundException e) { // Ignore } } // 如果父加载器未加载成功或者不需要首先委托 // 则使用WebappClassLoader加载器加载 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 } // 如果不需要委托,且本地加载器未加载成功 // 则再给父加载器加载 if (!delegateLoad) { if (log.isDebugEnabled()) log.debug(" Delegating to parent classloader at end: " + parent); try { clazz = Class.forName(name, false, parent); 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); }
到这里,基本就能明确了,默认情况下
JVM WebAppClassLoader
委托的情况下:
JVM JVM WebAppClassLoader
这里加载类的逻辑基本明白,但是我们还可以发现, WebAppClassLoader
还重写了 findClass()
@Override public Class<?> findClass(String name) throws ClassNotFoundException { checkStateForClassLoading(name); // Ask our superclass to locate this class, if possible // (throws ClassNotFoundException if it is not found) Class<?> clazz = null; clazz = findClassInternal(name); if ((clazz == null) && hasExternalRepositories) { clazz = super.findClass(name); } if (clazz == null) { throw new ClassNotFoundException(name); } return clazz; }
原本的代码很多,,这里删除了许多无关紧要的代码
这里可以看到大体的逻辑
findInternal()
方法查找并加载类 findInternal()
加载失败,则调用父类 findClass()
加载 protected Class<?> findClassInternal(String name) { checkStateForResourceLoading(name); if (name == null) { return null; } //转换为实际类名 String path = binaryNameToPath(name, true); //检查本地是否有缓存 ResourceEntry entry = resourceEntries.get(path); WebResource resource = null; if (entry == null) { //没有,则去实际路径加载 //这里路径便是: return getResource("/WEB-INF/classes" + path, true, true); //到这里我们就很熟悉了,这便是`Servlet`的规定路径 resource = resources.getClassLoaderResource(path); if (!resource.exists()) { return null; } entry = new ResourceEntry(); //记录修改时间,避免重复加载,同时也可以方便热加载 entry.lastModified = resource.getLastModified(); // Add the entry in the local resource repository synchronized (resourceEntries) { //这里再次确认其他线程是否已经加载了这个类 //这里考虑的确实比较细 ResourceEntry entry2 = resourceEntries.get(path); if (entry2 == null) { resourceEntries.put(path, entry); } else { entry = entry2; } } } //检查其他线程是否已经加载了这个类 Class<?> clazz = entry.loadedClass; //如果已经加载了,则直接返回 if (clazz != null) return clazz; //等待获取classLoading锁 synchronized (getClassLoadingLock(name)) { //获取到锁后,再次检查是否被其他线程加载 clazz = entry.loadedClass; if (clazz != null) return clazz; //这里防止entry不为null,但是`resource`为null的情况 //为什么要分开锁?不分开锁就不会存在此情况 if (resource == null) { resource = resources.getClassLoaderResource(path); } if (!resource.exists()) { return null; } //后续便是读取Mainfest文件以及其他文件, //读取完成后,再使用URLClassLoader加载整个jar包 byte[] binaryContent = resource.getContent(); Manifest manifest = resource.getManifest(); URL codeBase = resource.getCodeBase(); Certificate[] certificates = resource.getCertificates(); if (transformers.size() > 0) { // If the resource is a class just being loaded, decorate it // with any attached transformers // Ignore leading '/' and trailing CLASS_FILE_SUFFIX // Should be cheaper than replacing '.' by '/' in class name. String internalName = path.substring(1, path.length() - CLASS_FILE_SUFFIX.length()); for (ClassFileTransformer transformer : this.transformers) { try { byte[] transformed = transformer.transform( this, internalName, null, null, binaryContent); if (transformed != null) { binaryContent = transformed; } } catch (IllegalClassFormatException e) { log.error(sm.getString("webappClassLoader.transformError", name), e); return null; } } } // Looking up the package String packageName = null; int pos = name.lastIndexOf('.'); if (pos != -1) packageName = name.substring(0, pos); Package pkg = null; if (packageName != null) { pkg = getPackage(packageName); // Define the package (if null) if (pkg == null) { try { if (manifest == null) { definePackage(packageName, null, null, null, null, null, null, null); } else { definePackage(packageName, manifest, codeBase); } } catch (IllegalArgumentException e) { // Ignore: normal error due to dual definition of package } pkg = getPackage(packageName); } } try { clazz = defineClass(name, binaryContent, 0, binaryContent.length, new CodeSource(codeBase, certificates)); } catch (UnsupportedClassVersionError ucve) { throw new UnsupportedClassVersionError( ucve.getLocalizedMessage() + " " + sm.getString("webappClassLoader.wrongVersion", name)); } entry.loadedClass = clazz; } return clazz; }
可以看见,加载器最复杂的部分在于 findClassInternal
,涉及到许多多线程的问题。
不过,借此多了解下 URLClassLoader
也是很好的。