3、双亲委派模型详解

3. 双亲委派模型

  双亲委派模式要求除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器,请注意双亲委派模式中的父子关系并非通常所说的类继承关系,而是采用组合关系来复用父类加载器的相关代码,类加载器间的关系如下:

image

3.1 工作流程

  工作原理的是,如果一个类加载器收到了类加载请求,它并不会自己先去加载,而是把这个请求委托给父类的加载器去执行,如果父类加载器还存在其父类加载器,则进一步向上委托,依次递归,请求最终将到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就成功返回,倘若父类加载器无法完成此加载任务,子加载器才会尝试自己去加载,这就是双亲委派模式

image

3.2 应该叫做“父委派模型”而不是“双亲委派模型”

  “双亲委派”有很强的误导性,这是个翻译问题,实际上在oracle官方文档上是这样描述的:

The Java platform uses a delegation model for loading classes. The basic idea is that every class loader has a “parent” class loader. When loading a class, a class loader first “delegates” the search for the class to its parent class loader before attempting to find the class itself.

  java平台通过委派模型去加载类。每个类加载器都有一个父加载器。当需要加载类时,会优先委派当前所在的类的加载器的父加载器去加载这个类。如果父加载器无法加载到这个类时,再尝试在当前所在的类的加载器中加载这个类。

  所以,java的类加载机制应该叫做“父委派模型”,而不是“双亲委派机制”,“双亲委派机制”这个名字太具有误导性了。

3.3 双亲委派优势

  • 采用双亲委派模式的是好处是Java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子ClassLoader再加载一次。
  • 其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,通过双亲委托模式传递到启动类加载器,而启动类加载器在核心Java API发现这个名字的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。
  • 可能你会想,如果我们在classpath路径下自定义一个名为java.lang.SingleInterge类(该类是胡编的)呢?该类并不存在java.lang中,经过双亲委托模式,传递到启动类加载器中,由于父类加载器路径下并没有该类,所以不会加载,将反向委托给子类加载器加载,最终会通过系统类加载器加载该类。但是这样做是不允许,因为java.lang是核心API包,需要访问权限,强制加载将会报出如下异常
java.lang.SecurityException: Prohibited package name: java.lang

3.4 双亲委派什么时候被破坏

  • 通过预加载的方式;
  • 通过Thread.getContextClassLoader();

3.4.1 预加载

这里通过一个简单的例子,就拿sql连接来说:

  • (1)java.sql.DriverManager:rt.jar包中的类,通过Bootstrap加载器加载。
  • (2)DriverTest:开发人员自定义的实现了java.sql.Driver接口的类型,通过App加载器加载。
  开发人员通过DriverManager.registerDriver方法把自己实现的获取连接的Driver实现类加载并注册到DriverManager中。然后DriverManager.getConnection方法会遍历所有注册的Driver,并触发Driver的connect接口来获取连接。(即绕过在DriverManager所在的Bootstrap加载器,因为Bootstrap加载器不能加载开发人员实现的Driver类)

定义一个 DriverTest 类,实现rt.jar里面的java.sql.Driver接口

public class DriverTest implements Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new DriverTest());
            System.out.println("who load DriverTest: " + DriverTest.class.getClassLoader());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    @Override
    public Connection connect(String url, Properties info) throws SQLException {
        return new Connection() {
        //此处省略一堆代码......
        }
    }
    
    //启动代码
    public static void main(String[] args) {
        try {
            //由AppClassLoader加载DriverTest类
            Class.forName("com.jenson.pratice.classloader.DriverTest");
            System.out.println("who load DriverManager: "+DriverManager.class.getClassLoader());
            //通过rt.jar中的DriverManager去获取链接,DriverManager由BootstrapClassLoader加载
            Connection connection = DriverManager.getConnection("jdbc://");

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

此时运行main方法打印:

who load DriverTest: sun.misc.Launcher$AppClassLoader@18b4aac2
who load DriverManager: null
Process finished with exit code 0
---------------------
  DriverManager是由Bootstrap加载器的,因而获取不了Bootstrap加载器,所以为null。从父委派模型的机制上看,因为rt.jar是由Bootstrap加载器加载的,所以里面的类,都不能用到rt.jar以外的类。
————————————————

  那么DriverManager.getConnection是怎么调用DriverTest(App加载器)的getConnection方法呢?

因为父委派模型的限制,DriverManager不可能自己去加载DriverTest,DriverTest的加载实际上是由AppClassLoader完成的,DriverTest里面会往
DriverManager中注册一个驱动。

public class DriverTest implements java.sql.Driver {
    static {
        try {
            //在这里注册
            java.sql.DriverManager.registerDriver(new DriverTest());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

  对于DriverManager而言,他不关注driver的加载,他只需要遍历“registeredDrivers”,然后检查驱动类是否能被“调用类的类加载器”识别,如果可以识别,则调用driver.connect方法(即DriverTest中的实现)

 public class DriverManager{
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //省略一堆代码
        for(DriverInfo aDriver : registeredDrivers) {
        //在这里做安全校验
if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //在这里调用DriverTest的connect方法
                    Connection con = aDriver.driver.connect(url, info);
                    if (con != null) {
                        // Success!
                        println("getConnection returning " + aDriver.driver.getClass().getName());
                        return (con);
                    }
                } catch (SQLException ex) {
                    if (reason == null) {
                        reason = ex;
                    }
                }
                //省略一堆代码
                

整体的流程是这样的


image

3.4.2 Thread.getContextClassLoader()

1.通过调用Thread.getContextClassLoader()相当于直接拿到类加载器,自然不必通过向上递归走双亲委派了。

2.举个例子:rt.jar中的 javax.xml.parsers.FactoryFinder 中的 newInstance方法:

  • (1)在newInstance中会用到 getProviderClass 方法
  • (2)在getProviderClass中会用到 SecuritySupport.getContextClassLoader方法
  • (3)在SecuritySupport.getContextClassLoader中会用到Thread.currentThread().getContextClassLoader()拿到线程上下文类加载器
   /**
     * Create an instance of a class. Delegates to method
     * <code>getProviderClass()</code> in order to load the class.
     * 
     * 定义的JNDI接口
     * @param type Base class / Service interface  of the factory to instantiate
     * JNDI的实现类名
     * @param className Name of the concrete class corresponding to the service provider
     * 加载器:如果为null,则通过线程的上下文加载器进行加载
     * @param cl <code>ClassLoader</code> used to load the factory class. If <code>null</code>
     * current <code>Thread</code>'s context classLoader is used to load the factory class.
     * 如果为true,则使用bootstrap加载器。
     * @param useBSClsLoader True if cl=null actually meant bootstrap classLoader. This parameter
     * is needed since DocumentBuilderFactory/SAXParserFactory defined null as context classLoader.
     */
    static <T> T newInstance(Class<T> type, String className, ClassLoader cl,
                             boolean doFallback, boolean useBSClsLoader)
        throws FactoryConfigurationError
    {
        //省略一堆代码
        try {
            //在这个方法里面,可以通过线程上下文加载器进行加载className对应的类
            Class<?> providerClass = getProviderClass(className, cl, doFallback, useBSClsLoader);
            //省略一堆代码
            }
    }

    static private Class<?> getProviderClass(String className, ClassLoader cl,
            boolean doFallback, boolean useBSClsLoader) throws ClassNotFoundException
    {
        try {
            if (cl == null) {
                if (useBSClsLoader) {
                    return Class.forName(className, false, FactoryFinder.class.getClassLoader());
                } else {
                    //在这里,会获得线程的上下文加载器去加载类
                    //其中 ss是 SecuritySupport.java
                    cl = ss.getContextClassLoader();
                    if (cl == null) {
                        throw new ClassNotFoundException();
                    }
                    else {
                        return Class.forName(className, false, cl);
                    }
                }
            }
            //省略一堆代码
    }

class SecuritySupport  {
    ClassLoader getContextClassLoader() throws SecurityException{
        return (ClassLoader)
                AccessController.doPrivileged(new PrivilegedAction() {
            public Object run() {
                ClassLoader cl = null;
                //try {
                //获得线程的上下文加载器
                cl = Thread.currentThread().getContextClassLoader();
                //} catch (SecurityException ex) { }

                if (cl == null)
                    cl = ClassLoader.getSystemClassLoader();

                return cl;
            }
        });
    }

3.5 tomcat的类加载机制

  不只是Driver驱动的实现是这样,在tomcat、spring等等的容器框架也是通过一些手段去绕过“父委派机制”。例如下图中的tomat类加载器的结构:


image

从图中的委派关系中可以看出:

  • CommonClassLoader能加载的类都可以被Catalina - - - ClassLoader和SharedClassLoader使用,从而实现了公有类库的共用。
  • CatalinaClassLoader和Shared ClassLoader自己能加载的类则与对方相互隔离。
  • WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
  • JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.Class文件,它出现的目的就是为了被丢弃:当Web容器检测到JSP文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的HotSwap功能。

tomcat 违背了父委派模型吗?

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