源码讲解双亲委托如何被破坏

什么是双亲委托

双亲委托是类加载器的一个特性,当加载一个类的时候,首先会委托父加载器去加载,如果父加载器加载不到,才让子加载器尝试加载。

这样做的好处。
第一,保证了类的唯一性,不会被重复加载。
其次是安全性,你无法通过实现同包同名类来篡改jvm的内部类的行为。(想象一下你引入的jar包存在这种类,如果没有这种安全性做保证,会发生啥)

关于类加载器,我还想提两点

  1. 如果你使用A加载器加载了A类,那么A类中引用的B类,也会通过A加载器加载。不过A加载器加载B类的时候,还是会走双亲委托。
  2. 对于每个类加载器,他都会对应一些资源文件,这也确定哪些类会唯一被那个加载器加载。比如BootstarpClassLoader对应加载/jre/lib下的jar包。

破坏双亲委托案例

ServiceLoader

ServiceLoader是jvm内部类,它被BootstrapClassLoader所加载。但是ServiceLoader所加载的扩展点类,不能被BootstrapClassLoader加载。所以在ServiceLoader会通过它内部的持有
的loader类加载器加载这些扩展点类。


loader会在ServiceLoader构造的时候传入。


可以看到有两种情况,第一种使用我们传入的classloader,第二种使用线程上下文中的contextClassLoader。

对于第一种情况,如果传入的classloader为null,也可以理解为你想使用BootStrapClassLoader去加载扩展类,会强制替换为AppClassLoader


对于第二种情况,我们讲解下contextClassLoader绑定的classloader来自于哪里。

默认情况下,子线程的contextClassLoader会继承父线程的contextClassLoader,而最顶层的父线程也就是main线程,它的contextClassLoader为AppClassLoader。

当然你可以在使用ServiceLoader前,通过Thread.currentThread().setContextClassLoader(xxx);改变线程中的contextClassLoader。

举一个需要改变的例子。

比如的你的扩展点实现类存储在数据库中,这样就肯定用不了AppClassLoader了。

Thread.currentThread().setContextClassLoader(customeClassLoader);
ServiceLoader.load(xxxInterface.class)

哇塞,这好像是一种远程spi的解决方案。

个人感觉ServiceLoader这种处理方式,没有破坏双亲委托,在类加载器内部还是符合双亲委托的,只算是破坏了常规的类加载流程,这种取巧方式只是为了解决这种特定的问题。而tomcat的WebAppClassLoader那是真的破坏了双亲委托。

Tomcat WebAppClassLoader

在我们实现自定义ClassLoader的时候,推荐我们只实现 findClass方法,而不建议我们重载loadClass方法。

为啥?因为双亲委托就封装在这个方法中,如果我们破坏,可能会带来不可预知的风险。比如一个类加载了两份。

但是Tomcat真正破坏了双亲委托,显然是修改了这个方法。

看下WebappClassLoade的loadClass方法实现

    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

        synchronized (getClassLoadingLock(name)) {
            if (log.isDebugEnabled())
                log.debug("loadClass(" + name + ", " + resolve + ")");
            Class<?> clazz = null;

            // Log access to stopped class loader
            checkStateForClassLoading(name);
            //缓存
            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;
            }

            String resourceName = binaryNameToPath(name, false);

            ClassLoader javaseLoader = getJavaseClassLoader();
            boolean tryLoadingFromJavaseLoader;
            try {
                URL url;
                if (securityManager != null) {
                    PrivilegedAction<URL> dp = new PrivilegedJavaseGetResource(resourceName);
                    url = AccessController.doPrivileged(dp);
                } else {
                    url = javaseLoader.getResource(resourceName);
                }
                tryLoadingFromJavaseLoader = (url != null);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                tryLoadingFromJavaseLoader = true;
            }


            //如果是jvm内部类 走javaseLoader=extclassloader
            if (tryLoadingFromJavaseLoader) {
                try {
                    clazz = javaseLoader.loadClass(name);
                    if (clazz != null) {
                        if (resolve)
                            resolveClass(clazz);
                        return clazz;
                    }
                } catch (ClassNotFoundException e) {
                    // Ignore
                }
            }

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

            //delegateLoad=true 表示先走commonclassloader
            //filter方法用于判断当前加载类是否为tomcat本身需要加载的类
            //delegate默认为false
            boolean delegateLoad = delegate || filter(name, true);

            //delegateLoad=true的情况下,交由parent加载
            if (delegateLoad) {
                if (log.isDebugEnabled())
                    log.debug("  Delegating to parent classloader1 " + 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
                }
            }

            //从我们的war包路径下搜索类
            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为false,交由parent加载
            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);
    }

tomcat8在默认配置下,类加载器继承结构为

     Bootstrap
          |
         EXT
          |
         APP
          |
       Common
       /     \
  Webapp1   Webapp2 ...

如果在 conf/catalina.properties对server.loader 或shared.loader 做了配置,类加载器继承结构如下

  Bootstrap
      |
     EXT
      |
     APP
      |
    Common
     /  \
Server  Shared
         /  \
   Webapp1  Webapp2 ...

我们默认使用第一种方式讲解,也是是上面源码中的parent为CommonClassLoader

CommonClassLoader默认加载以下路径的资源

common.loader="${catalina.base}/lib","${catalina.base}/lib/*.jar","${catalina.home}/lib","${catalina.home}/lib/*.jar"

修改后的loadClass方法大致逻辑如下(delegate默认为false)

  1. 先走class加载缓存
  2. 如果class为jvm内置类,走extClassLoader
  3. 如果filter方法返回true,走commonClassLoader
  4. 通过WebAppClassLoader加载/WEB-INF/classes和/WEB-INF/lib/*.jar中的类
  5. 如果filter方法返回false,走commonClassLoader

filter方法内的大致逻辑是,判断该class是否是tomcat自己的类。



也就是是否是tomcat源码中这些类。

从loadClass逻辑中,对于webapp中的类,直接通过WebAppClassLoader去加载,不走双亲委托机制。

这种做法能保证tomcat中部署的2个应用的隔离。如果没有这个改造,那么A应用加载版本1的类后,B应用也被迫使用这个版本,如果B依赖的版本和A不同,那么B应用可能会发生错误。

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