Tomcat WebAppClassLoader隔离加载实现

1 概述

本文属于阅读代码中的笔记,主要通过Servlet的加载介绍Tomcat中WebappClassLoader对于每个Web app的隔离加载机制。

2 一个自定义ClassLoader的例子

在介绍具体加载机制之前,我们先看一个关于类加载的例子:

package xyf.classloader.demo;

import java.io.File;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;


public class ClassloaderDemo {

    public static void main(String[] args) throws Exception {

        MyClassLoader loader = new MyClassLoader();
        
        //使用MyClassLoader加载cl.test.A
        Class aClazz = loader.loadClass("cl.test.A");
        Object a = aClazz.newInstance();
        
        Method getB = aClazz.getMethod("getB", null);
        Object b = getB.invoke(a, null);
        
        System.out.println(a.getClass().getClassLoader());
        System.out.println(b.getClass().getClassLoader());
    }

}


class MyClassLoader extends URLClassLoader {
    //该路径是一个非classpath路径
    public static File file = new File("D:\\developer\\java\\cltest");
    
    //MyClassLoader从上面的非classpath路径加载类
    public MyClassLoader() throws Exception {
        super(new URL[] {file.toURL()}, null, null);
    }
}

//下面类A和B编译成.class文件后放到非classpath路径
//D:\\developer\\java\\cltest下面
package cl.test;
public class A{
    B b = new B();
    public B getB() {
        return b;
    }
}

package cl.test;
public class B{}

/*
上面ClassloaderDemo.main运行之后输出结果为:
MyClassLoader@5c647e05
MyClassLoader@5c647e05

说明:
可见如果一个类由某个类加载器加载,那么因为使用该类(本例中A)而加载的类
(本例中B)也会使用加载同一个类加载器

当然,如果我们将类B放在classpath下,然后将Thread.currentThread.getContextClassLoader()
作为参数传入URLClassLoader构造函数,作为其双亲加载器,那么类B将由其双亲加载器加载,因为
URLClassLoader也遵循双亲委派模型
*/

为什么先介绍这样一个自定义类加载器的例子呢?其实在Tomcat中,基本上一个Web App所有的操作都是由一个Servlet启动的,我们在定义自己的Servlet时可能会使用其他的三方类库,比如MyBatis。结合着上面的例子可以知道,如果我们在加载Servlet时使用的是一个自定义的ClassLoader类实例,那么该Servlet中引用的三方类库,如:MyBatis也会由该ClassLoader加载。再假设我们为每个Web App都专门实例化一个ClassLoader实例,那么这样就做到了不同Web App的隔离。因为我们知道Java中一个类是由该类名称以及该类的defining loader定义的。

具体什么是defining loader,可以看jvms8中关于类加载器的介绍

A class loader L may create C by defining it directly or by delegating to another class loader. If L creates C directly, we say that L defines C or, equivalently, that L is the defining loader of C.

When one class loader delegates to another class loader, the loader that initiates the loading is not necessarily the same loader that completes the loading and defines the class. If L creates C, either by defining it directly or by delegation, we say that L initiates loading of C or, equivalently, that L is an initiating loader of C.

At run time, a class or interface is determined not by its name alone, but by a pair: its binary name (§4.2.1) and its defining class loader. Each such class or interface belongs to a single run-time package. The run-time package of a class or interface is determined by the package name and defining class loader of the class or interface.

3 StandardContext实例化

现在看Tomcat是如何加载web app目录(一般是webapps目录)里的web app的,在HostConfig.start会进行应用部署:

//HostConfig
 public void start() {

    ...

    if (host.getDeployOnStartup())
        //进行应用部署
        deployApps();

}

protected void deployApps() {

    File appBase = host.getAppBaseFile();
    File configBase = host.getConfigBaseFile();
    String[] filteredAppPaths = filterAppPaths(appBase.list());
    // Deploy XML descriptors from configBase
    deployDescriptors(configBase, configBase.list());
    // Deploy WARs
    deployWARs(appBase, filteredAppPaths);
    // Deploy expanded folders
    //我们主要看如何部署webapps里的应用
    deployDirectories(appBase, filteredAppPaths);

}

protected void deployDirectories(File appBase, String[] files) {

    if (files == null)
        return;

    ExecutorService es = host.getStartStopExecutor();
    List<Future<?>> results = new ArrayList<>();
    //对于webapps下面的所有目录,进行以次部署,一般一个目录
    //表示一个单独的web app
    for (int i = 0; i < files.length; i++) {

        if (files[i].equalsIgnoreCase("META-INF"))
            continue;
        if (files[i].equalsIgnoreCase("WEB-INF"))
            continue;
        File dir = new File(appBase, files[i]);
        if (dir.isDirectory()) {
            ContextName cn = new ContextName(files[i], false);

            if (isServiced(cn.getName()) || deploymentExists(cn.getName()))
                continue;
            //并行部署多个web app
            //DeployDirectory构造函数第一个参数为HostConfig,这里
            //传的是this
            results.add(es.submit(new DeployDirectory(this, cn, dir)));
        }
    }

    for (Future<?> result : results) {
        try {
            result.get();
        } catch (Exception e) {
            log.error(sm.getString(
                    "hostConfig.deployDir.threaded.error"), e);
        }
    }
}

下面看DeployDirectory的定义,DeployDirectoryHostConfig的一个内部类:

//HostConfig.DeployDirectory
private static class DeployDirectory implements Runnable {

    private HostConfig config;
    private ContextName cn;
    private File dir;

    public DeployDirectory(HostConfig config, ContextName cn, File dir) {
        this.config = config;
        this.cn = cn;
        this.dir = dir;
    }

    @Override
    public void run() {
        //这里调用HostConfig的deployDirectory进行单个web app(或者说单个)
        //目录的部署
        //config就是上面代码构造函数传的this
        config.deployDirectory(cn, dir);
    }
}

HostConfig.deployDirectory的方法体比较长,下面是省略了非主要部分之后的代码:

protected void deployDirectory(ContextName cn, File dir) {


    long startTime = 0;
    // Deploy the application in this directory
    ...
    Context context = null;
    File xml = new File(dir, Constants.ApplicationContextXml);
    File xmlCopy =
            new File(host.getConfigBaseFile(), cn.getBaseName() + ".xml");


    DeployedApplication deployedApp;
    boolean copyThisXml = isCopyXML();
    boolean deployThisXML = isDeployThisXML(dir, cn);

    try {
        if (deployThisXML && xml.exists()) {
            synchronized (digesterLock) {
                try {
                    //最主要的代码就是这里,这里会创建一个StandardContext
                    //类实例,也就是每个web app都会对应一个
                    //StandardContext实例
                    context = (Context) digester.parse(xml);
                } catch (Exception e) {
                    log.error(sm.getString(
                            "hostConfig.deployDescriptor.error",
                            xml), e);
                    context = new FailedContext();
                } finally {
                    digester.reset();
                    if (context == null) {
                        context = new FailedContext();
                    }
                }
            }

            ...
        } else if (!deployThisXML && xml.exists()) {
            // Block deployment as META-INF/context.xml may contain security
            // configuration necessary for a secure deployment.
            log.error(sm.getString("hostConfig.deployDescriptor.blocked",
                    cn.getPath(), xml, xmlCopy));
            context = new FailedContext();
        } else {
            //contextClass = org.apache.catalina.core.StandardContext
            //同样是实例化一个StandardContext的实例
            context = (Context) Class.forName(contextClass).getConstructor().newInstance();
        }

        Class<?> clazz = Class.forName(host.getConfigClass());
        LifecycleListener listener = (LifecycleListener) clazz.getConstructor().newInstance();
        context.addLifecycleListener(listener);

        context.setName(cn.getName());
        context.setPath(cn.getPath());
        context.setWebappVersion(cn.getVersion());
        context.setDocBase(cn.getBaseName());
        host.addChild(context);
    } catch (Throwable t) {
        ...
    } finally {
        ...
    }

    deployed.put(cn.getName(), deployedApp);

    ...
}

上面介绍了每个web app都会对应一个StandardContext类实例,阅读过Tomcat源码的读者都知道Tomcat中许多类都实现了LifecycleListener接口,实现了生命周期管理。StandardContext同样也实现了该接口,那么我们接下来就看StandardContext.startInternal方法的定义,该方法同样较长,因为我们这里主要介绍类加载机制,所以我们省略了其他不相关的部分:

//StandardContext
@Override
protected synchronized void startInternal() throws LifecycleException {

    ...
    //每个StandardContext对象都持有一个WebappLoader对象,也就是自己的
    //类加载器,所有该StandardContext加载的三方类和其他StandardContext
    //加载的三方类是隔离的
    if (getLoader() == null) {
        //getParentClassLoader返回的parentClassLoader是其父类加载器
        //是由CopyParentClassLoaderRule.begin中配置的,通过digester
        //注入的,实现share加载
        WebappLoader webappLoader = new WebappLoader(getParentClassLoader());
        webappLoader.setDelegate(getDelegate());
        setLoader(webappLoader);
    }
    ...
}

WebappLoader内部持有一个具体的ClassLoader实现,其实例化在该类start时,WebappLoader同样实现了LifecycleListener接口,StandardContext在start时会调用WebappLoader的start方法,所以这里看下其startInternal方法的定义:

protected void startInternal() throws LifecycleException {

    ...
    try {
        //创建具体的ClassLoader实例
        classLoader = createClassLoader();
        classLoader.setResources(context.getResources());
        classLoader.setDelegate(this.delegate);

        // Configure our repositories
        setClassPath();

        setPermissions();

        ((Lifecycle) classLoader).start();

        String contextName = context.getName();
        if (!contextName.startsWith("/")) {
            contextName = "/" + contextName;
        }
        ObjectName cloname = new ObjectName(context.getDomain() + ":type=" +
                classLoader.getClass().getSimpleName() + ",host=" +
                context.getParent().getName() + ",context=" + contextName);
        Registry.getRegistry(null, null)
            .registerComponent(classLoader, cloname, null);

    } catch (Throwable t) {
       ...
    }

    setState(LifecycleState.STARTING);
}

private WebappClassLoaderBase createClassLoader()
    throws Exception {
    //loaderClass是其私有变量
    //private String loaderClass = 
    //ParallelWebappClassLoader.class.getName();
    Class<?> clazz = Class.forName(loaderClass);
    WebappClassLoaderBase classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = context.getParentClassLoader();
    }
    Class<?>[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor<?> constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoaderBase) constr.newInstance(args);

    return classLoader;
}

下面我们看一下ParallelWebappClassLoader的类继承关系图:

ParallelWebappClassLoader类继承关系.png

可见ParallelWebappClassLoader类是URLClassLoader类的一个实现,所有要从中加载类,必须设置其要加载的类所在的URL,ParallelWebappClassLoader父类WebappClassLoaderBase同样实现了Lifecycle接口,所以看其start方法实现:

//WebappClassLoaderBase
public void start() throws LifecycleException {

    state = LifecycleState.STARTING_PREP;
    //设置其加载类的URL为/WEB-INF/classes和/WEB-INF/lib
    WebResource classes = resources.getResource("/WEB-INF/classes");
    if (classes.isDirectory() && classes.canRead()) {
        localRepositories.add(classes.getURL());
    }
    WebResource[] jars = resources.listResources("/WEB-INF/lib");
    for (WebResource jar : jars) {
        if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
            localRepositories.add(jar.getURL());
            jarModificationTimes.put(
                    jar.getName(), Long.valueOf(jar.getLastModified()));
        }
    }

    state = LifecycleState.STARTED;
}
//上面start方法将URL放入localRepositories中,所以WebappClassLoaderBase
//重写了getURLs方法如下
@Override
public URL[] getURLs() {
    ArrayList<URL> result = new ArrayList<>();
    result.addAll(localRepositories);
    result.addAll(Arrays.asList(super.getURLs()));
    return result.toArray(new URL[result.size()]);
}

到此已经介绍了每个Web app特有的StandardContext及其持有的ClassLoader是如何实例化的,下面看Servlet是如何加载的。

4 Servlet加载

要介绍Servlet加载首先要看下StandardWrapper类,StandardWrapper是Servlet的封装,在web.xml中配置的每个servlet-class都会对应一个StandardWrapperStandardWrapper.servletClass(String类型,还未加载)对应其servlet-class具体配置。

不管是在启动时加载Servlet还是在第一个请求到来时加载Servlet都会调用StandardWrapper.load方法。

在介绍StandardWrapper.load方法之前,我们首先看下InstanceManager,每个StandardContext都会持有一个InstanceManager实例,StandardContext.InstanceManager会在StandardContext.startInternal中实例化,默认的InstanceManager实现是DefaultInstanceManager,DefaultInstanceManager会持有一个ClassLoader实例,该ClassLoader实例其实就是StandardContext持有的WebappLoader.classLoader

//StandardContext
protected synchronized void startInternal() throws LifecycleException {
    ...
    if (ok ) {
        if (getInstanceManager() == null) {
            javax.naming.Context context = null;
            if (isUseNaming() && getNamingContextListener() != null) {
                context = getNamingContextListener().getEnvContext();
            }
            Map<String, Map<String, String>> injectionMap = buildInjectionMap(
                    getIgnoreAnnotations() ? new NamingResourcesImpl(): getNamingResources());
            //实例化InstanceManager对象实例,默认实现为
            //DefaultInstanceManager
            setInstanceManager(new DefaultInstanceManager(context,
                    injectionMap, this, this.getClass().getClassLoader()));
        }
        getServletContext().setAttribute(
                InstanceManager.class.getName(), getInstanceManager());
        InstanceManagerBindings.bind(getLoader().getClassLoader(), getInstanceManager());
    }
    ...
}

//DefaultInstanceManager构造函数
public DefaultInstanceManager(Context context,
        Map<String, Map<String, String>> injectionMap,
        org.apache.catalina.Context catalinaContext,
        ClassLoader containerClassLoader) {
    //获取`StandardContext`持有的`WebappLoader.classLoader`
    classLoader = catalinaContext.getLoader().getClassLoader();
    privileged = catalinaContext.getPrivileged();
    //containerClassLoader是加载StandardContext类的类加载器
    this.containerClassLoader = containerClassLoader;
    ignoreAnnotations = catalinaContext.getIgnoreAnnotations();
    Log log = catalinaContext.getLogger();
    Set<String> classNames = new HashSet<>();
    ...
}

介绍了InstanceManager之后,下面看StandardWrapper.load方法:

//StandardWrapper
@Override
public synchronized void load() throws ServletException {
    //根据servletClass(String)加载Servlet实例
    instance = loadServlet();

    //初始化
    if (!instanceInitialized) {
        initServlet(instance);
    }

    if (isJspServlet) {
        ...
    }
}

public synchronized Servlet loadServlet() throws ServletException {

    ...
    //获取StandardContext持有的InstanceManager对象实例
    InstanceManager instanceManager = ((StandardContext)getParent()).getInstanceManager();
    try {
        //通过InstanceManager加载Servlet
        servlet = (Servlet) instanceManager.newInstance(servletClass);
    } catch (ClassCastException e) {
    ...
}

下面看DefaultInstanceManager.newInstance是如何实例化类的:

//DefaultInstanceManager
@Override
public Object newInstance(String className) throws IllegalAccessException,
        InvocationTargetException, NamingException, InstantiationException,
        ClassNotFoundException, IllegalArgumentException, NoSuchMethodException, SecurityException {
    //首先根据类名加载Class对象
    Class<?> clazz = loadClassMaybePrivileged(className, classLoader);
    return newInstance(clazz.getConstructor().newInstance(), clazz);
}


protected Class<?> loadClassMaybePrivileged(final String className,
        final ClassLoader classLoader) throws ClassNotFoundException {
    Class<?> clazz;
    if (SecurityUtil.isPackageProtectionEnabled()) {
        try {
            clazz = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {

                @Override
                public Class<?> run() throws Exception {
                    //实际调用loadClass加载类
                    return loadClass(className, classLoader);
                }
            });
        } catch (PrivilegedActionException e) {
            Throwable t = e.getCause();
            if (t instanceof ClassNotFoundException) {
                throw (ClassNotFoundException) t;
            }
            throw new RuntimeException(t);
        }
    } else {
        //实际调用loadClass加载类
        clazz = loadClass(className, classLoader);
    }
    checkAccess(clazz);
    return clazz;
}

protected Class<?> loadClass(String className, ClassLoader classLoader)
        throws ClassNotFoundException {
    //如果是Tomcat内部类,则只使用containerClassLoader尝试加载
    //containerClassLoader是构造函数中传入的加载StandardContext类的加载器
    //这是和其他StandardContext共用的加载器
    if (className.startsWith("org.apache.catalina")) {
        return containerClassLoader.loadClass(className);
    }
    try {
        //如果不是Tomcat内部类,同样先使用containerClassLoader进行加载
        //所以Servlet中引用的三方类会先使用share版本
        Class<?> clazz = containerClassLoader.loadClass(className);
        if (ContainerServlet.class.isAssignableFrom(clazz)) {
            return clazz;
        }
    } catch (Throwable t) {
        ExceptionUtils.handleThrowable(t);
    }
    //如果不是上述情景,则使用该StandardContext自己的类加载器进行加载
    return classLoader.loadClass(className);
}

5 总结

结合第2节的例子,以及每个StandardContext都持有一个自己的ClassLoader实例,就可以知道Tomcat WebAppClassLoader隔离加载的机制

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容