jvm

JVM 系列 类加载机制

开启JVM探索新篇章

Posted by lichao modified on November 7, 2019

推荐:CSDN

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

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

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

案例分析

JDBC

1
2
3
4
// Mysql 注册驱动及获取 connection 的过程
String url = "jdbc:mysql://localhost:3306/testdb";    
// 通过 Java 库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password"); 

从 Java1.6 开始自带的 jdbc4.0 版本已支持 SPI 服务加载机制,只要 Mysql 的 jar 包在类路径中,就可以注册 Mysql 驱动

注册驱动

重点在 DriverManager.getConnection() 中。由于调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

1
2
3
4
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
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加载驱动类。读取 META-INF/services 下文件中的类名,使用 ThreadContextClassLoader 加载
    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;
    }
    

    // 方式二:通过 System.getProperty("jdbc.drivers") 获取设置,然后通过系统类加载器加载。
    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);
        }
    }
}

注意 driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法。Class.forName(DriverName, false, loader) 代码所在的类在 java.util.ServiceLoader 类中,而ServiceLoader.class 又加载在 BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader。这时候只能使用 ThreadContextClassLoader 了,也就是说把自己加载不了的类加载到 ThreadContextClassLoader 中(通过Thread.currentThread()获取)。ThreadContextClassLoader 默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

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

ContextClassLoader 默认存放了 AppClassLoader 的引用,由于它是在运行时被放在了线程中,所以不管当前程序处于何处(BootstrapClassLoader或是ExtClassLoader等),在任何需要的时候都可以用Thread.currentThread().getContextClassLoader()取出应用程序类加载器来完成需要的操作。

驱动实现类就是com.mysql.jdbc.Driver.Class,com.mysql.jdbc.Driver加载后运行的静态代码块:

1
2
3
4
5
6
7
8
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()。它最终调用了以下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
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");
 }

Class.forName 是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)和 Class.forName(String className)。第一种形式的参数 name表示的是类的全名;initialize表示是否初始化类;loader表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize的值为 true,loader的值为当前类的类加载器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

由于 ThreadContextClassLoader 本质就是当前应用类加载器,所以之前的初始化就是加载在当前的类加载器中,这一步就是校验存放的driver是否属于调用者的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠ThreadContextClassLoader了。