《Java架构师的第一性原理》25Java基础之Java虚拟机第2篇类加载器

1. 类加载器

简单说下JVM预定义的三种类型的类加载器,这个也算是老生常谈了。当JVM启动一个项目的时候,它将缺省使用以下三种类型的类加载器:
1. 启动(Bootstrap)类加载器:负责装载<Java_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包。由native方法实现加载过程,程序无法直接获取到该类加载器,无法对其进行任何操作。
2. 扩展(Extension)类加载器:扩展类加载器由sun.misc.Launcher.ExtClassLoader实现的。负责加载<Java_Home>/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库。程序可以访问并使用扩展类加载器。
3. 系统(System)类加载器:系统类加载器是由sun.misc.Launcher.AppClassLoader实现的,也叫应用程序类加载器。负责加载系统类路径-classpath-Djava.class.path变量所指的目录下的类库。程序可以访问并使用系统类加载器。

2. 双亲委派类加载机制

2.1 类加载器的父子关系

三种类加载器的父子关系如图所示

注意这儿的父子并不是继承的意思,它们都是ClassLoader抽象类的实现,因此都含有一个ClassLoader parent成员变量,该变量指向其父加载器,类似单向链表。

2.2 双亲委派源码实现

委派关系也被称为代理,我们来看看代码,loadClass是抽象类ClassLoader中的类加载的核心方法。

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 若本加载器之前是否已加载过,直接取缓存,native方法实现
            Class c = findLoadedClass(name);
            if (c == null) {
                try {
                    // 只要有父加载器就先委派父加载器来加载
                    if (parent != null) {
                        // 注意此处递归调用
                        c = parent.loadClass(name, false);
                    } else {
                        // ext的parent为null,因为Bootstrap是无法被程序被访问的,默认parent为null时其父加载器就是Bootstrap
                        // 此时直接用native方法调用启动类加载加载,若找不到则抛异常
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 对ClassNotFoundException不做处理,仅用作退出递归
                }

                if (c == null) {
                    // 如果父加载器无法加载那么就在本类加载器的范围内进行查找
                    // findClass找到class文件后将调用defineClass方法把字节码导入方法区,同时缓存结果
                    c = findClass(name);
                }
            }
            // 是否解析,默认false
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

可以看出所谓的双亲委派的本质就是这两句递归代码:

if (parent != null) {
    c = parent.loadClass(name, false);
}

加载成功就得到Class对象c,失败就抛异常然后前一级方法用catch抓住并忽略,再进行当前类加载器的findClass()操作,如此反复。

注意
1. 出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类
2. 类加载后将进入连接(link)阶段,它包含验证、准备、解析,resolve参数决定是否执行解析阶段,jvm规范并没有严格指定该阶段的执行时刻
3. 由于先使用findLoadedClass()查找缓存,相同的类只会被加载一次

3. 用户自定义类加载器

当你自己写一个类实现了ClassLoader后,那么它就是用户自定义类加载器了。实例化自定义类加载器时,若不指定父类加载器(不把父ClassLoader传入构造函数)的情况下,默认采用系统类加载器(AppClassLoader)。对应的无参默认构造函数实现如下:

protected ClassLoader() {
    this(checkCreateClassLoader(), getSystemClassLoader());
}

它将调用有参构造函数,将getSystemClassLoader()取到的系统类加载器作为parent传入(最后一节详述)。因此用户自定义类加载器也可以通过双亲委派的方式获取到那3个类加载器加载的类对象了。

当实现自定义类加载器时不应重写loadClass(),除非你不需要双亲委派机制。要重写的是findClass()的逻辑,也就是寻找并加载类的方式。

使用自定义类加载器获取到的Class对象需通过newInstance()获取实例,要比较具有相同类全限定名的两个Class对象是否是同一个,取决于是否是同一类加载器加载了它们,也就是调用defineClass()的那个类加载器,而非之前委派的类加载器。

4. 常用方法分析

4.1 java.lang.Class对象的方法

4.1.1  Class<?> forName(……)

这是手动加载类的常见方式,在Class类中有两个重载:

  • public static Class<?> forName(String className)
  • public static Class<?> forName(String name, boolean initialize,
    ClassLoader loader)

第二个构造函数指定了父类加载器,这儿可能要有疑问了,第一个方法默认使用哪个类加载器来加载的呢?我们来看下具体实现:

public static Class<?> forName(String className)
            throws ClassNotFoundException {
    // 使用native方法获取调用类的Class对象
    Class<?> caller = Reflection.getCallerClass();
    return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

其中getClassLoader(caller)设置了所使用的类加载器,继续看其实现:

 static ClassLoader getClassLoader(Class<?> caller) {
     if (caller == null) {
         return null;
     }
     return caller.getClassLoader0();
 }
}

这段代码的官方注解是“返回caller的类加载器”,即native方法getClassLoader0()返回调用者的类加载器。也就是说假设在A类里执行forName(String className),那么所使用的ClassLoader就是加载A的ClassLoader。

提示
forName0()本质还是调用ClassLoaderloadClass()来加载类。

● ClassLoader getClassLoader()

该方法用于获取加载某Class对象的类加载器,可是通过实例或类对象来获取:

  • (new A()).getClass().getClassLoader()
  • A.class.getClassLoader()

4.1.2 各种获取类信息的方法

反射得到Class对象后通过以下方法获取类信息:

Field[] getDeclaredFields()
Class[] getDeclaredClasses()
Method[] getDeclaredMethods()

等等,详情可查阅javadoc或查看源码

4.2 java.lang.ClassLoader对象的方法

● ClassLoader getParent()

获取父ClassLoader

● Class loadClass(String)

显式调用该方法来进行类加载,传入类全限定名

● URL getResource(String)

获取具有给定名称的资源定位符。资源可以是任何数据,名称须以“/”分离路径名。实际调用findResource()方法,该方法无实现,需子类继承实现。

● InputStream getResourceAsStream(String)

获取可以读取资源的InputStream输入流,实际上就是用上面的方法获取到URL后调用url.openStream()得到 InputStream。

● ClassLoader getSystemClassLoader()

这是一个静态方法,通过ClassLoader.getSystemClassLoader()便可获取到系统类加载器AppClassLoader, 和调用类无关。具体实现见最后一小节。

5. URLClassLoader

概述

ClassLoader只是一个抽象类,很多方法是空的需要自己去实现,比如 findClass()findResource()等。而java提供了java.net.URLClassLoader这个实现类,适用于多种应用场景。

之前提到的AppClassLoaderExtClassLoader都是URLClassLoader的子类,自定义类加载器推荐直接继承它。

来看下javadoc中的描述:

该类加载器用于从一组URL路径(指向JAR包或目录)中加载类和资源。约定使用以 ‘/’结束的URL来表示目录。如果不是以该字符结束,则认为该URL指向一个JAR文件。

5.1 构造函数

URLClassLoader接受一个URL数组为参数,它将在这些提供的路径下加载所需要的类,对应的主要构造函数有

  • public URLClassLoader(URL[] urls)
  • URLClassLoader(URL[] urls, ClassLoader parent)

5.2 getURLs()方法

使用URL[] getURLs()方法可以获取URL路径,参考代码:

public static void main(String[] args) {
    URL[] urls = ((URLClassLoader)ClassLoader.getSystemClassLoader()).getURLs();
    for (URL url : urls) {
        System.out.println(url);
    }
}
// file:/D:/Workbench/Test/bin/

5.3 加载方式

findClass()中其使用了URLClassPath类中的Loader类来加载类文件和资源。URLClassPath类中定义了两个Loader类的实现,分别是FileLoaderJarLoader类,顾名思义前者用于加载目录中的类和资源,后者是加载jar包中的类和资源。Loader类默认已经实现getResource()方法,即从网络URL地址加载jar包然后使用JarLoader完成后续加载,而两个实现类不过是重写了该方法。

URLClassPath是如何选择使用正确的Loader的呢?答案是——根据URL格式而定。下面是删减过的核心代码,简单易懂。

private Loader getLoader(final URL url)
{
    String s = url.getFile();
    // 以"/"结尾时,若url协议为"file"则使用FileLoader加载本地文件
    // 否则使用默认的Loader加载网络url
    if(s != null && s.endsWith("/"))
    {
        if("file".equals(url.getProtocol()))
            return new FileLoader(url);
        else
            return new Loader(url);
    } else {
        // 非"/"结尾则使用JarLoader
        return new JarLoader(url, jarHandler, lmap);
    }
}

5.4 getSystemClassLoader()方法的实现

追溯getSystemClassLoader()的源码可以发现其实质上是通过sun.misc.Launcher实例获取返回其成员变量loader的。那这个loader是何时赋值的呢?我们来看下它的构造函数(删减了不相关的内容):

  public Launcher()
  {
      ExtClassLoader extclassloader;
      try
      {
      // 创建并初始化扩展类加载器ExtClassLoader
          extclassloader = ExtClassLoader.getExtClassLoader();
      }
      catch(IOException ioexception)
      {
          throw new InternalError("Could not create extension class loader");
      }
      try
      {
          // 创建并初始化系统类加载器AppClassLoader,设置其父类加载器为ext,最后传给loader
          loader = AppClassLoader.getAppClassLoader(extclassloader);
      }
      catch(IOException ioexception1)
      {
          throw new InternalError("Could not create application class loader");
      }
      // 默认将线程上下文类加载器设置为AppClassLoader
      // 相关信息见另一篇博文
      Thread.currentThread().setContextClassLoader(loader);
  }

可以看到Launcher初始化时创建生成了ExtClassLoaderAppClassLoader,并将线程上下文类加载器默认设置为了AppClassLoader。虽然没去看jvm的源码,但我推测jvm可能就是通过创建Launcher实例来完成扩展和系统类加载器的创建的,而启动(Bootstrap)类加载器的创建则是另外调用本地方法完成的。

很明显,getSystemClassLoader()返回的loader就是AppClassLoader无误,这儿我们也发现了线程上下文类加载器赋值处,具体有关线程上下文类加载器的学习请参考底部的另一篇博文。

6. 线程上下文类加载器

此前我对线程上下文类加载器(ThreadContextClassLoader,下文使用TCCL表示)的理解仅仅局限于下面这段话:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由应用程序类加载器(Application ClassLoader)**来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

一直困恼我的问题就是,它是如何打破了双亲委派模型?又是如何逆向使用类加载器了?直到今天看了jdbc的驱动加载过程才茅塞顿开,其实并不复杂,只是一直没去看代码导致理解不够到位。

6.1 JDBC案例分析

6.1.1 JDBC的DriverManager

我们先来看平时是如何使用mysql获取数据库连接的:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName("com.mysql.jdbc.Driver").newInstance(); 
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写的Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

初始化方法loadInitialDrivers()的代码如下:

private static void loadInitialDrivers() {
    String drivers;
    try {
        // 先读取系统属性
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // 通过SPI加载驱动类
    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
                // Do nothing
            }
            return null;
        }
    });
    // 继续加载系统属性中的驱动类
    if (drivers == null || drivers.equals("")) {
        return;
    }
    
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            // 使用AppClassloader加载
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是:

  1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载;
  2. 通过System.getProperty("jdbc.drivers")获取设置,然后通过系统类加载器加载。

6.1.2 JDBC中的SPI

下面详细分析SPI加载的那段代码。

先来看看什么是SP机制,引用一段博文中的介绍:

SPI机制简介

SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
SPI具体约定
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

知道SPI的机制后,我们来看刚才的代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}
private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,复习双亲委派加载机制请看:java类加载器不完整分析 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码,的确如此:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

6.1.3 校验实例的归属

好,刚才说的驱动实现类就是com.mysql.jdbc.Driver.Class,它的静态代码块里头又写了什么呢?是否又用到了TCCL呢?我们继续看下一个例子。

com.mysql.jdbc.Driver加载后运行的静态代码块:

static {
    try {
        // Driver已经加载到TCCL中了,此时可以直接实例化
        java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());
    } catch (SQLException E) {
        throw new RuntimeException("Can't register driver!");
    }
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(
     String url, java.util.Properties info, Class<?> caller) throws SQLException {
     /* 传入的caller由Reflection.getCallerClass()得到,该方法
      * 可获取到调用本方法的Class类,这儿获取到的是当前应用的类加载器
      */
     ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
     synchronized(DriverManager.class) {
         if (callerCL == null) {
             callerCL = Thread.currentThread().getContextClassLoader();
         }
     }

     if(url == null) {
         throw new SQLException("The url cannot be null", "08001");
     }

     SQLException reason = null;
     // 遍历注册到registeredDrivers里的Driver类
     for(DriverInfo aDriver : registeredDrivers) {
         // 检查Driver类有效性
         if(isDriverAllowed(aDriver.driver, callerCL)) {
             try {
                 println("    trying " + aDriver.driver.getClass().getName());
                 // 调用com.mysql.jdbc.Driver.connect方法获取连接
                 Connection con = aDriver.driver.connect(url, info);
                 if (con != null) {
                     // Success!
                     return (con);
                 }
             } catch (SQLException ex) {
                 if (reason == null) {
                     reason = ex;
                 }
             }

         } else {
             println("    skipping: " + aDriver.getClass().getName());
         }

     }
     throw new SQLException("No suitable driver found for "+ url, "08001");
 }
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
        // 传入的classLoader为调用getConnetction的当前类加载器,从中寻找driver的class对象
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }
    // 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个
    // driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoader
        result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

由于TCCL本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader。

例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。

6.2 Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/*”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示

 

 

灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

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

6.3 Spring加载问题

Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

解答

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
    try {
        // 创建WebApplicationContext
        if (this.context == null) {
            this.context = createWebApplicationContext(servletContext);
        }
        // 将其保存到该webapp的servletContext中        
        servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
        // 获取线程上下文类加载器,默认为WebAppClassLoader
        ClassLoader ccl = Thread.currentThread().getContextClassLoader();
        // 如果spring的jar包放在每个webapp自己的目录中
        // 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoader
        if (ccl == ContextLoader.class.getClassLoader()) {
            currentContext = this.context;
        }
        else if (ccl != null) {
            // 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来
            // 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出
            currentContextPerThread.put(ccl, this.context);
        }
        
        return this.context;
    }
    catch (RuntimeException ex) {
        logger.error("Context initialization failed", ex);
        throw ex;
    }
    catch (Error err) {
        logger.error("Context initialization failed", err);
        throw err;
    }
}

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。

总结

通过上面的两个案例分析,我们可以总结出线程上下文类加载器的适用场景:

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

98. 说人话Q&A

1)Java类加载器有哪几种?分别加载哪里的类?

BootstrapClassLoader  ====>  <Java_Home>/lib

ExtClassLoader            ====>  <Java_Home>/lib/ext 或 -Djava.ext.dir

AppClassLoader     ====>  -classpath 或 -Djava.class.path

2)双亲委派模型(Parents Delegation Model)的定义是什么?

(1)除了顶层的启动类加载器外,其它的类加载器都要有自己的父类加载器(这里的父子关系一般通过组合关系(Composition)来实现,而不是继承关系(Inheritance))。

(2)一个类加载器首先将类加载请求转发到父类加载器,只有当父类加载器无法完成时才尝试自己加载。

3)双亲委派模型的本质是什么?

双亲委派的本质就是这两句递归代码:

if (parent != null) {
    c = parent.loadClass(name, false);
}

4)双亲委派模型的好处是什么?

(1)使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。

(2)出于安全考虑,Bootstrap启动类加载器只加载包名为java、javax、sun等开头的类;

5)Java SPI机制是什么?

Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里

的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。

基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。

6)Java SPI的实现原理是什么?

(1)jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

(2)Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootstrapLoader中,因此传给 forName 的 loader 必然不能是BootstrapLoader。(否则就死循环了)

(3)所以ServiceLoader只能使用TCCL,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取)

(4)这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。

那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(BootstrapLoader Classloader)来加载的;SPI的实现类是由应用程序类加载器(Application ClassLoader)**来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

7)问:spi 接口和实现类。 不就跟object类和其他子类 是一样的道理吗? 有什么特殊的吗?其他的都可以。为什么说spi就不行?非要一个线程上下文加载器?

问题举例

 假如加载一个类person。 我们来看看如何加载的。

  • 首先加载他的父类object。根据双亲委派模型。object会被委托给BootstrapClassloader加载;
  • 加载person类,使用BootstrapClassLoader去加载 Person类,这是没法加载到的, 根据双亲委派模型会被AppClassLoader加载;
  • AppClassLoader能加载到Person类(AppClassLoader顾名思义就是加载你自己写的app系统路径嘛,肯定能找到person.class) 

(1)spi机制时候,首页有这个共识:java里要某个类要引用另外一个类,需要先加载class文件(可能jvm已经加载过就直接使用),而且默认是用加载当前类的加载器去加载。

DriverManager类是jdk定义的类,是要用BootstrapClassloader加载,当用DM去获取Conn时,conn实现类是第三方mysql,Oracle等实现的插件jar包,

拿着加载DM的BootstrapClassloader去加载这些三方实现类肯定加载不到啊(BootstrapClassloader是加载jre/lib路径的),

根据类加载的原则(加载类的时候,会默认使用当前类的类加载器去加载),也就是说这里文中提到的用户自定义的类),。

只能采用TCCL了,而jdk的util提供的serviceLoader工具类就是用了TCCL,可以直接使用这个工具类来把三方实现类加载进来,这样DM就拿到conn了。

(2)SPI需要用到 ServiceLoader 这个类,而这个类处于原生的包中(/JAVA_HOME/lib 下),所以,ServiceLoader 类就只能被BootstrapClassLoader加载。

(3)直接由BootstrapLoader加载的SPI实现类,加载不到。

(4)类被加载器加载是有条件的,不是说一启动就会立即加载,只有在使用时才会加载class文件。

(5)ServiceLoader 此处双亲委派机制不是从系统加载器->扩展加载器->引导加载器往上的三层结构,而是只有引导加载器一层,因此如果引导加载器未加载到,且它下面没有子加载器,因此会直接抛出异常.所以必须借助线程上下文类加载器。

8)问:为什么启动类加载器加载不了spi实现类?如果启动类加载器加载不了,spi实现类一样也可以使用双亲委派来加载啊?看了一些博客举的jdbc的例子,也没看出为什么。也许我对类加载器理解的还不够深刻。

启动类加载器只会加载固定目录下的类,你可以使用System.getProperty("sun.boot.class.path") System.getProperty("java.ext.dirs") System.getProperty("java.class.path") 分别取查看每种类加载器的默认加载路径。所以启动类加载不了当前classpath下的类。

如果一个类是由启动类加载器加载的那么所依赖的类也会是启动类加载的,但是启动类加载类又加载不了当前classpath下的类,所以需要通过线程上下文拿到可以加载classpath的类加载器进行加载。

9)为什么jdk核心类库接口的实现类非得由BootstrapClassLoader加载,系统类加载器不行吗,那我自己定义的类实现jdk的接口,它也能运行呀?

 运行是可以运行,但只影响到系统类加载器中的类,启动类加载器限制加载为了保证安全性,比如引入的第三方jar包里自己定义一个java.lang.String类,启动类加载器优先加载了它,那整个系统的信息就被轻易玩弄于黑客股掌之中了。线程上下文类加载器算是是为了在扩展性和安全性之间的折中吧。

10)线程上下文类加载器TheadContextClassLoader是什么?

(1)TCCL破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

(2)TCCL本质就是当前应用类加载器。

11)线程上下文类加载器到底有没有“破坏”双亲委派模型?

你好,请问加载驱动实现类的线程上下文类加载器(不进行set的话,默认是AppClassLoader)重写loadClass方法了吗?如果没有,那就是没有破坏双亲委派模型。只不过在DriverManager接口的静态代码块中显式使用了系统类加载器加载了classpath中的jar包实现,而DriverManager是启动类加载器加载的,仅此而已。

破坏的定义方式一:绝大多数文章都认为loadClass方法是双亲委派模型的实现,我想你也不会否认吧,这个方法确实包含了所有双亲委派模型的逻辑。findClass方法的设计是为了让自定义类加载器时不必重写loadClass方法,不破坏双亲委派模型,从这个侧面也可以看出,不重写findClass就是不破坏双亲委派模型。

破话的定义方式二:双亲委派正常情况下是APP主动找类,先让EXT找,EXT又先让BOOT找,上面都找不到APP再自己找。现在这种情况却是BOOT主动找类,我们清楚BOOT可定是找不到的,如果此时BOOT找不到就说我找不到了,那是符合双亲委派模型的;但是,为了还能找到,BOOT又把这个事情给TCCL(APP)了,任务到了TCCL(APP)这里,TCCL(APP)本身其实是可以找到的,然而TCCL(APP)本身未破坏双亲委派模型,所以又按照正常的流程抛了一遍,最后还得是自己找到。我猜是这里给了你一种未破坏双亲委派模型的假象。我理解的关键点是要搞清楚谁发起加载类这个事情的,然后又是如何终结的,然后再看是否符合双亲委派模型。

12)问:博主您好,我有一个问题。像您示例中讲的JDBC那样,DriverManager是由BootStrapClassLoader加载的,而mysql的Driver是由TCCL加载的,根据我的理解,父类加载器加载的类是无法访问子类加载器加载的类(类加载器命名空间的影响),那么为什么还能再DriverManager访问mysql的Driver呢?还是说我对类加载器命名空间的理解有误?

答:不知道你有没完全看完这篇文章哈,DriverManager并没有直接访问mysql的Driver,而是通过SPI加载了mysql的Driver到TCCL中,Driver的静态初始化代码中把自己的实例放到了DriverManager的DriverList中。

问:可是在DriverMananger.getConnection()方法中的Connection con = aDriver.driver.connect(url, info);使用到了mysql的Driver啊。

答:Driver.driver是一个定义在jdk里的接口

class DriverInfo {
    final Driver driver;
}

mysql的Driver(实现类)是在运行时将自己的实例放入其中的,connect也是该接口本身定义好的方法,此时DriverMananger只是通过接口调用connect方法,并不需要加载mysql的class,它只是在调用接口的方法。

问:可是在运行时driver.connect()肯定是实现类的connect方法啊,不应该是接口的connect()方法吧。我现在有一种理解:加载A类的类加载器如果加载不了B类,A类可以通过其他的类加载去加载B类,比如通过TCCL加载,那么A类也就能访问B类了,我理解的对吗?

答:类只有在初始化时才会触发加载机制,运行driver.connect()时已经是新建过的对象了,早已在TCCL中加载好了,所以启动类加载器不会再去加载一次的,而是直接调用

问:请问一下:也就是说调用类和BootStrapClassLoader不能访问AppClassLoader加载的类,但是能访问AppClassLoader的加载类的对象?访问对象不会触发类加载吗?

答:嗯,我是这么理解的,对象只在被主动引用(包括new)时才会触发类加载,并且是在调用时被当前类加载器加载,后续就不会再触发了。

13)线程上下文类加载器TheadContextClassLoader的适用场景?

(1)当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。

(2)当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。

99. 直接读这些牛人的原文

java类加载器不完整分析

真正理解线程上下文类加载器(多案例分析)

类加载器

深入理解Java类加载器(ClassLoader)

深入理解Java类加载器(1):Java类加载原理解析

 

使用自定义ClassLoader解决反序列化serialVesionUID不一致问题

利用类加载器解决不兼容的Jar包共存的问题

老大难的 Java ClassLoader 再不理解就老了

https://github.com/sofastack/sofa-ark

 

posted @ 2023-12-21 13:58  沙漏哟  阅读(19)  评论(0编辑  收藏  举报