JVM类加载器(五)

我们知道,每个类都会尝试使用自己的类加载器去加载依赖的类。如果ClassX引用ClassY,那么ClassX的类加载器会尝试加载ClassY,前提是ClassY尚未被加载。但这种做法有可能出现一个问题,如果一个根类加载器所加载的类,要去引用一个classpath下的类,是不是会出现问题?我们知道,classpath下的类只能由应用类加载器或者我们自己定义的类加载器去加载,根类加载器由特定的加载路径,classpath对于根类加载器是不可见的。

也许有人疑问,为什么会有根类加载器下的类引用classpath下的类的情况呢?其实是有的,而且大部分人都遇到过。不过这里我们要先看一下java.util.ServiceLoader类的定义:

一个简单的服务提供者加载设施。服务 是一个熟知的接口和类(通常为抽象类)集合。服务提供者 是服务的特定实现。提供者中的类通常实现了接口,并将服务本身中定义类作为子类。服务提供者可以以扩展的形式安装在Java平台的实现中,也就是将jar 文件放入任意常用的扩展目录中。也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用。唯一强制要求的是,提供者类必须具有不带参数的构造方法,以便它们可以在加载中被实例化。

通过在资源目录META-INF/services中放置提供者配置文件 来标识服务提供者。文件名称是服务类型的完全限定二进制名称。该文件包含一个具体提供者类的完全限定二进制名称列表,每行一个。忽略各名称周围的空格、制表符和空行。注释字符为'#'('\u0023', NUMBER SIGN);忽略每行第一个注释字符后面的所有字符。文件必须使用 UTF-8 编码。

以延迟方式查找和实例化提供者,也就是说根据需要进行。服务加载器维护到目前为止已经加载的提供者缓存。每次调用 iterator 方法返回一个迭代器,它首先按照实例化顺序生成缓存的所有元素,然后以延迟方式查找和实例化所有剩余的提供者,依次将每个提供者添加到缓存。可以通过 reload 方法清除缓存。

以上来源于Java API里的说明,也许有人第一次看有点头晕,我们可以简单的认为:ServiceLoader也像ClassLoader一样,能装载类文件,但是使用时有区别,具体区别如下:(1) ServiceLoader装载的是一系列有某种共同特征的实现类,而ClassLoader是个万能加载器;(2)ServiceLoader装载时需要特殊的配置,使用时也与ClassLoader有所区别;(3)ServiceLoader还实现了Iterator接口。

我们先在我们工程的pom.xml文件中加入MySQL JDBC的依赖:

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.20</version>
</dependency>

  

在上面的MySQL JDBC中,有实现一个com.mysql.jdbc.Driver类,这个类是java.sql.Driver的实现,于是我们定义一个以Driver为泛型的ServiceLoader,我们通过这个loader获取一个迭代器,这个迭代器会打印我们工程中所加载的java.sql.Driver的实现:

package com.leolin.jvm;

import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;

public class MyTest26 {
    public static void main(String[] args) {
        ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);
        Iterator<Driver> iterator = loader.iterator();
        while (iterator.hasNext()) {
            Driver driver = iterator.next();
            System.out.println("driver:" + driver.getClass() + ", loader:" + driver.getClass().getClassLoader());
        }
        System.out.println("当前线程上下文类加载器:" + Thread.currentThread().getContextClassLoader());
        System.out.println("ServiceLoader的类加载器:" + ServiceLoader.class.getClassLoader());
    }
}

  

运行结果:

driver:class com.mysql.cj.jdbc.Driver, loader:sun.misc.Launcher$AppClassLoader@18b4aac2
当前线程上下文类加载器:sun.misc.Launcher$AppClassLoader@18b4aac2
ServiceLoader的类加载器:null

  

通过打印,我们知道ServiceLoader这个类是由根加载器所加载的,但是ServiceLoader所返回的迭代器,又加载了com.mysql.jdbc.Driver这个类,而这个类却是应用类加载器所加载的,是处于classpath下的,按理说对于根加载器是不可见的,那么在ServiceLoader是如何定位到classpath下的com.mysql.jdbc.Driver类呢?我们试着走进ServiceLoader.load(Class service)这个方法:

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

public static <S> ServiceLoader<S> load(Class<S> service,
										ClassLoader loader)
{
	return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
	service = Objects.requireNonNull(svc, "Service interface cannot be null");
	loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
	acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
	reload();
}

public void reload() {
	providers.clear();
	lookupIterator = new LazyIterator(service, loader);
}

private class LazyIterator
	implements Iterator<S>
{

	Class<S> service;
	ClassLoader loader;
	……

	private LazyIterator(Class<S> service, ClassLoader loader) {
		this.service = service;
		this.loader = loader;
	}
	……
}

    

在ServiceLoader.load(Class service)方法中,会从当前线程获得一个上下文类加载器,这个类加载器会赋值给ServiceLoader的成员变量loader,最后生成一个懒加载迭代器,懒加载器迭代器正是拿着这个上下文类加载器去遍历实现java.sql.Driver的类。而根据上一节的讲解,我们知道这个上下文类加载器一般是我们的应用类加载器,所以即便ServiceLoader是由根加载器所加载,但这个迭代器拿到应用类加载器,就可以扫描classpath下实现了java.sql.Driver的类。

接下来我们看看mysql-connector-java-8.0.20.jar这个文件,为什么懒加载迭代器能找到它呢?之前我们说过查找服务实现的要求:

  • 通过在资源目录META-INF/services中放置提供者配置文件 来标识服务提供者。
  • 文件名称是服务类型的完全限定二进制名称。
  • 该文件包含一个具体提供者类的完全限定二进制名称列表,每行一个。

这是mysql-connector-java-8.0.20.jar的目录结构:

 

mysql-connector-java-8.0.20.jar\META-INF\services\java.sql.Driver下的内容:

com.mysql.cj.jdbc.Driver

  

可以看到,正是因为mysql-connector-java-8.0.20.jar是按照之前ServiceLoader查找服务实现的规范,所以ServiceLoader能找到MySQL JDBC的实现。

上面所说的,正是著名的SPI的概念:SPI的英文全名为Service Provider Interface,是Java提供一套规范接口,而由第三方的厂商针对接口来实现各自的服务。典型的案例就有就是我们刚才所举的JDBC。

在SPI接口的代码中,使用线程上下文类加载器就可以加载到SPI实现的类,线程上下文类加载器在很多SPI的实现中,都会得到相应的使用。当高层提供了统一的接口让底层去实现,但高层又需要加载底层的实现,同时又要再高层加载或实例化底层的一些类的时候,就必须要通过线程上下文类加载器来帮助高层的ClassLoader来找到并且加载底层的实现类。

线程上下文加载器是从JDK1.2开始引入的,Thread类中的getContextClassLoader()与 setContextClassLoader(ClassLoader loader)分别用于获取和设置线程上下文类加载器。如果没有通过setContextClassLoader(ClassLoader loader)方法设置的话,线程将继承父线程的上下文类加载器,Java应用运行时的初始线程的上下文类加载器是系统类加载器。该线程中运行的代码可以通过该类加载器加载类和资源。

父ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的ClassLoader加载的类,这就改变了父加载器加载的类无法使用子加载器或是其他没有父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型。
线程上下文类加载器就是当前线程的Current ClassLoader。在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托父加载器进行加载。但是有些接口是Java核心库所提供的的(如JDBC),Java核心库是由启动类记载器去加载的,而这些接口的实现却来自不同的jar包(厂商提供),Java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足要求。通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载。

也许有人会问,那如果高层有需要加载底层的实现,为什么不用ClassLoader.getSystemClassLoader()呢?因为这个应用类加载器默认就是线程上下文加载器,但我们要知道,系统类加载器仅仅只能加载classpath下的jar包,如果我们有一个实现,位于classpath之外的目录呢?但我们又不想去修改默认的系统类加载器为我们自己实现的类加载器呢?

好了,我们现在了解了线程上下文类加载器的作用。我们再来看看下面这段代码,这段代码相信很多人都复制黏贴过,我们重点来讲讲这段代码如果执行功能所发生的事。

package com.leolin.jvm;

import java.sql.Connection;
import java.sql.DriverManager;

public class MyTest27 {
    public static void main(String[] args) throws Exception {
        Class.forName("com.mysql.jdbc.Driver");
        Connection conn = DriverManager.getConnection("jdbc:mysql//localhost:3306/mytest", "username", "password");
    }
}

  

我们先进到Class.forName(String className)这个静态方法中看看,这个方法先是获取调用者的类型,再获取类型对应的ClassLoader,最后调用forName0这个本地方法,传入类型的二进制名字和调用者对应的ClassLoader,对类型进行初始化。

public static Class<?> forName(String className)
			throws ClassNotFoundException {
	Class<?> caller = Reflection.getCallerClass();
	return forName0(className, true, ClassLoader.getClassLoader(caller), caller);
}

private static native Class<?> forName0(String name, boolean initialize,
										ClassLoader loader,
										Class<?> caller)
	throws ClassNotFoundException;

 

接着我们来分析分析,在com.mysql.jdbc.Driver进行初始化时,都做了什么:

package com.mysql.jdbc;

import java.sql.SQLException;

public class Driver extends com.mysql.cj.jdbc.Driver {
    public Driver() throws SQLException {
    }

    static {
        System.err.println("Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary.");
    }
}

  

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

  

可以看到,在初始化com.mysql.jdbc.Driver时,会调用DriverManager.registerDriver(Driver driver)将自身的驱动类型注册进去。那我们来看看DriverManager这个类:

	private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
	……
    public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
    }
	……
    public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        /* Register the driver if it has not already been added to our list */
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

    }
	……

  

驱动类型会注册进registeredDrivers这个写时复制的列表,如果驱动类型不存在这个列表时则进行注册。

接着是从DriverManager中获取一个连接:

public static Connection getConnection(String url,
	String user, String password) throws SQLException {
	java.util.Properties info = new java.util.Properties();

	if (user != null) {
		info.put("user", user);
	}
	if (password != null) {
		info.put("password", password);
	}

	return (getConnection(url, info, Reflection.getCallerClass()));
}

private static Connection getConnection(
	String url, java.util.Properties info, Class<?> caller) throws SQLException {
	
	ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
	synchronized(DriverManager.class) {
		// synchronize loading of the correct classloader.
		if (callerCL == null) {
			callerCL = Thread.currentThread().getContextClassLoader();
		}
	}

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

	println("DriverManager.getConnection(\"" + url + "\")");

	SQLException reason = null;

	for(DriverInfo aDriver : registeredDrivers) {
		// If the caller does not have permission to load the driver then
		// skip it.
		if(isDriverAllowed(aDriver.driver, callerCL)) {
			try {
				println("    trying " + aDriver.driver.getClass().getName());
				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;
				}
			}

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

	}

	// if we got here nobody could connect.
	if (reason != null)    {
		println("getConnection failed: " + reason);
		throw reason;
	}

	println("getConnection: no suitable driver found for "+ url);
	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 {
			aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
		} catch (Exception ex) {
			result = false;
		}

		 result = ( aClass == driver.getClass() ) ? true : false;
	}

	return result;
}

  

可以看到,当我们传入连接地址、用户名和密码后,会遍历registeredDrivers这个列表,传入连接地址、用户名、密码,尝试获取一个连接,如果连接获取成功则返回。这里我们多介绍一个isDriverAllowed方法,这个方法用于判断我们所注册的驱动,是否和我们当前的调用者的类加载器同属于一个类加载器的名字空间下。

posted @ 2020-05-09 21:56  北洛  阅读(267)  评论(0编辑  收藏  举报