破坏双亲委派模型

上接: https://www.cnblogs.com/ronnieyuan/p/11885463.html

简介

  • 双亲委派模型并不是一个强制性的约束模型, 而是Java设计者推荐给开发者的类加载器实现方式。
  • 在Java的世界中大部分的类加载器都遵循这个模型, 但也有例外, 历史上出现过3次较大规模的双亲委派模型"被破坏"情况。

三次较大规模的破坏

  • 第一次发生在双亲委派模型出现之前(jdk1.2发布之前)。

    • 由于类加载器和抽象类java.lang.ClassLoader 在JDK1.0时代就已经存在, 面对已经存在的用户自定义类加载器的实现代码, Java 设计者引入双清委派模型时不得不做出一些妥协。

    • 为了向前兼容, jdk1.2之后的java.lang.ClassLoader 添加了一个新的 protected 方法 findClass()

       /**
           * Finds the class with the specified <a href="#name">binary name</a>.
           * 根据特定的二进制名来查找该类。
           * This method should be overridden by class loader implementations that
           * follow the delegation model for loading classes, and will be invoked 
           * by the {@link #loadClass <tt>loadClass</tt>} method after checking the
           * parent class loader for the requested class. 
           * 该方法应该被实现了类加载器的的类重载, 二次遵循双亲委派模型, 并且在检查请求的
           * 类的父类加载之后会被loadClass方法调用
           * The default implementation throws a <tt>ClassNotFoundException</tt>.
           *
           * @param  name
           *         The <a href="#name">binary name</a> of the class
           *
           * @return  The resulting <tt>Class</tt> object
           *
           * @throws  ClassNotFoundException
           *          If the class could not be found
           *
           * @since  1.2
           */
          protected Class<?> findClass(String name) throws ClassNotFoundException {
              throw new ClassNotFoundException(name);
          }
      
    • 在此之前, 用户去继承java.long.Classloader 的唯一目的是为了重写loadClass() 方法

    • 因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(), 而该方法的唯一逻辑就是去调用自己的loadClass()

       // This method is invoked by the virtual machine to load a class.
       // 该方法有虚拟机调用来加载类
          private Class<?> loadClassInternal(String name)
              throws ClassNotFoundException
          {
              // For backward compatibility, explicitly lock on 'this' when
              // the current class loader is not parallel capable.
              // 为了向后兼容性, 当前类加载器不是可并行的时,显示地锁了当前对象
              if (parallelLockMap == null) {
                  synchronized (this) {
                       return loadClass(name);
                  }
              } else {
                  return loadClass(name);
              }
          }
      
  • 第二次发生是由模型自身的缺陷导致, 咋么处理基础类调用回用户的代码?

    • 双亲委派很好地解决了各个类加载器的基础类统一问题 (越基础的类由越上层的加载器进行加载)

    • 基础类通常作为被用户代码调用的API, 但是当基础类要调用回用户的代码时该咋么解决?

    • 典型例子: JNDI(Java Naming and Directory Interface)服务,

      • 它的代码由启动类加载器去加载(jdk1.3植入的rt.jar)。

      • JNDI的目的就是对资源进行集中管理和查找, 它需要调用独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者 (SPI, Service Provider Interface) 的代码, 但启动类加载器不认识这些代码。

      • 为了解决该问题, Java设计团队只能引入一个不太优雅的设计: 线程上下文类加载器(Thread Context ClassLoader)。

      • 这个类加载器可以通过java.lang.Thread 类的setContextClassLoader() 方法进行设置, 如果创建线程时还未设置, 它将会从父线程中继承一个, 如果在应用程序的全局范围内都没有设置过的话, 那该类加载器默认就是应用程序类加载器。

            /**
             * Sets the context ClassLoader for this Thread. The context
             * ClassLoader can be set when a thread is created, and allows
             * the creator of the thread to provide the appropriate class loader,
             * through {@code getContextClassLoader}, to code running in the thread
             * when loading classes and resources.
             *
             * <p>If a security manager is present, its {@link
             * SecurityManager#checkPermission(java.security.Permission) checkPermission}
             * method is invoked with a {@link RuntimePermission RuntimePermission}{@code
             * ("setContextClassLoader")} permission to see if setting the context
             * ClassLoader is permitted.
             *
             * @param  cl
             *         the context ClassLoader for this Thread, or null  indicating the
             *         system class loader (or, failing that, the bootstrap class loader)
             *
             * @throws  SecurityException
             *          if the current thread cannot set the context ClassLoader
             *
             * @since 1.2
             */
            public void setContextClassLoader(ClassLoader cl) {
                SecurityManager sm = System.getSecurityManager();
                if (sm != null) {
                    sm.checkPermission(new RuntimePermission("setContextClassLoader"));
                }
                contextClassLoader = cl;
            }
        
      • JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码, 也就是父类加载器请求子类加载器去完成类加载的动作。

      • 这种行为实际上就是打通了双亲委派模型的层次结构来逆向使用类加载器, 实际上违背了双亲委派模型的一般性原则。

      • Java中所有涉及 SPI的加载动作基本上都采用这种方式, 如JNDI, JDBC, JCE(Java Cryptography Extension), JAXB(JavaArchitectureforXMLBinding) 和 JBI(Java Business Integration)等。

  • 第三次发生时由于用户对程序动态性的追求而导致的。

    • 如: 代码热替换(Hotswap), 模块热部署(Hot Deployment) 等, 说白了急速希望应用程序向计算机外设那样, 接上鼠标, U盘, 不用重启机器技能立即使用, 鼠标有问题或要升级就换个鼠标, 不用停机也不用重启。
    • 生产系统需要尽可能的不重启, 重启一次可能就被认定为生产事故, 这种情况下热部署就对企业级软件开发者有很大的吸引力。
    • OSGI(开放服务网关协议,Open Service Gateway Initiative) 目前是业界的Java模块化标准, 而OSGI实现模块化热部署的关键则是它自定义的类加载器机制的实现。
    • 每一个程序模块(Bundle) 都有一个自己的类加载器, 当需要换一个Bundle时, 就把Bundle连同类加载器一起换掉以实现代码的热替换。
    • OSGI环境下, 类加载器不再是双亲委派模型中的树状结构, 而是进一步发展为更加复杂的网状结构, 当收到类加载请求时, OSGI 将按照下面的顺序进行类搜索:
      1. 将以java.* 开头的类委派给父类加载器加载。
      2. 否则, 将委派列表名单以内的类委派给父类加载器加载。
      3. 否则, 将Import列表中的类委派给 Export 这个类的Bundle 的类加载加载。
      4. 否则, 查找当前Bundle的 ClassPath, 使用自己的类加载器加载。
      5. 否则, 查找类是否在自己的Fragment Bundle中, 如果在, 则委派给 Fragment Bundle的类加载器加载。
      6. 否则, 查找 Dynamic Import(动态导入) 列表的 Bundle, 委派给对应Bundle的类加载器。
      7. 否则, 类查找失败
    • 上述的查找顺序只有开头两点仍然符合双亲委派规则, 其余的类查找都是在平级的类加载器中进行的。
    • OSGI中对类加载器的使用是值得学习的。
原文地址:https://www.cnblogs.com/ronnieyuan/p/11975584.html