Tomcat 源码剖析(八)Tomcat 是如何加载webapps中的类的

Tomcat 是如何加载webapps中的类的?

【问题】

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来说,各个类加载器的父子关系如下:

image

Tomcat中,还可以通过配置是否委托来打破双亲委派机制:

对于WebApp加载器,如果按照双亲委派,那么在需要加载某个类的时候,会首先委派给JVM的加载器,如果没有加载成功则再委派给Common类加载器,接下来依次是Shared类加载器,WebApp类加载器。

但是在Tomcat实现中默认并不是这样,而是首先交给JVM类加载器加载,如果JVM加载器没有加载成功,再从WebApp类中加载,如果还是没有加载成功,再委托给SharedCommon类加载器加载。这样便是违背了双亲委派机制。

为什么要这么实现呢?个人认为是Tomcat认为一般用户不会提供多余的类,并且在实际开发中ShareCommon目录用的非常少,因此默认直接取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

Bootstrap###createClassLoader()

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

ClassLoaderFactory###createClassLoader()

//。。。
return AccessController.doPrivileged(
        new PrivilegedAction() {
            @Override
            public URLClassLoader run() {
                if (parent == null)
                    return new URLClassLoader(array);
                else
                    return new URLClassLoader(array, parent);
            }
        });

这个方法比较长,前面还有挺多代码,大概是验证URL的有效性,这里先省略

这里可以看到,这三个类加载其实最底层都是URLClassLoader,关于URLClassLoader,我们以后再说,这里我们重点关注下WebAppClassLoader


WebAppClassLoader

  • 初始化

StandardContext##startInternal()

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的时候,便是使用的这个ContextclassLoader

 classLoader = catalinaContext.getLoader().getClassLoader();

DefaultInstanceManager正是用来加载Servlet的工具类。


说远了,这里一句话就是WebAppClassLoader便是用来加载ServletClassLoader

下面来看看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中。

WebappClassLoaderBase##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()

WebappClassBase##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()

WebappClassBase##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()加载
  • 如果还是加载失败,则抛出异常

WebappClassBase##findClassInternal()

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也是很好的。