双亲委托机制的局限性
当一个类由类加载器A加载,那么这个类的依赖类也由相同的类加载器加载器,在双亲委托模型下,类的加载是自上而下的,即下层的类加载器会委托上层加载。对于SPI来说,很多接口是由JAVA核心类库提供的,JAVA核心类库是由根类加载器来加载的,而这些接口的实现来着不同的jar包,由不同的厂商提供,根类加载不会去加载这些jar包中的类,这样传统的双亲委托机制就无法满足SPI的要求。而通过给当前线程设置线程上下文类加载器,在需要加载厂商实现类的地方,从当前线程获取上下文类加载器去加载,从而满足SPI。
线程上下文类加载器
线程上下文类加载器(Thread Context Class Loader),通过Thread.getContextClassLoader()和Thread.setContextClassLoader(ClassLoader cl)分别获取和设定。如果没有通过setContextClassLoader方法设置的话,线程将继承父线程的上下文类加载器。父类加载器加载的类可以使用Thread.currentThread().getContextClassLoader()设定的classloader加载的类。这一特性打破了双亲委托机制。
根类加载器无法记载项目类路径下的类,但是可以通过当前线程的上下文类加载器来加载,相当于给类加载开了外挂。
默认情况下,当前线程的上下文类加载器是系统类加载器。
1 public class TestThreadContextClassLoader {
2 public static void main(String[] args) {
3 System.out.println(Thread.currentThread().getContextClassLoader());
4 System.out.println(Thread.class.getClassLoader());
5
6 }
7 }
执行后输出结果为:
1 sun.misc.Launcher$AppClassLoader@18b4aac2
2 null
3
4 Process finished with exit code 0
线程上下文类加载器的使用模式一般为:获取-使用-还原。
1 ClassLoader loader = Thread.currentThread().getContextClassLoader();// 原来的类加载器
2
3 try {
4 Thread.currentThread().setContextClassLoader(loader1);// 设置classloader,以便userMethod()使用
5 userMethod();
6 } finally {
7 Thread.currentThread().setContextClassLoader(loader);//还原
8 }
探究Service Loader
以Mysql的驱动加载为例,探究一下java的Service Loader的使用。首先在依赖加入mysql-connector,
1 <dependency>
2 <groupId>mysql</groupId>
3 <artifactId>mysql-connector-java</artifactId>
4 <version>5.1.48</version>
5 </dependency>
然后使用Service loader加载驱动:
1 package io.dongzhi;
2
3 import java.sql.Driver;
4 import java.util.Iterator;
5 import java.util.ServiceLoader;
6
7 public class TestServiceLoader {
8 public static void main(String[] args) {
9 ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class);
10 Iterator<Driver> it = serviceLoader.iterator();
11 while (it.hasNext()) {
12 Driver driver = it.next();
13 System.out.println("driver class:" + driver );
14 System.out.println("driver class loader:" + driver.getClass().getClassLoader());
15 }
16 }
17 }
打印结果为:
1 driver class:com.mysql.jdbc.Driver@2b193f2d
2 driver class loader:sun.misc.Launcher$AppClassLoader@18b4aac2
3 driver class:com.mysql.fabric.jdbc.FabricMySQLDriver@355da254
4 driver class loader:sun.misc.Launcher$AppClassLoader@18b4aac2
5
6 Process finished with exit code 0
也就是说成功加载了2个驱动:com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver,它们的类加载器都是系统类加载器。
该案例的疑问是,代码中并没有出现和mysql driver相关的代码,ServiceLoader.load(Driver.class)引用的也是JDK自带的接口,为何能够成功打印出mysql的两个驱动实现类呢?翻阅Service Loader的java doc相关说明和源码得知,在mysql-connector-xx.jar中的META-INF/services/目录下有这样一个文件java.sql.Driver,其内容为:
1 com.mysql.jdbc.Driver
2 com.mysql.fabric.jdbc.FabricMySQLDriver
可以猜测,ServiceLoader.load(Driver.class)这行代码一定读取了该文件,并加载了这2个驱动实现类。
下面接着简单看一下源码。
1 public static <S> ServiceLoader<S> load(Class<S> service) {
2 ClassLoader cl = Thread.currentThread().getContextClassLoader();
3 return ServiceLoader.load(service, cl);
4 }
由上面的讨论可知,由于java.util.ServiceLoader是java核心类,所以它的类加载器是BootstrapClassLoader,在上面的源码中获取了当前线程的上下文类加载器(默认为AppClassLoader),为后续加载classpath下的两个驱动类做准备。
1 public static <S> ServiceLoader<S> load(Class<S> service,
2 ClassLoader loader)
3 {
4 return new ServiceLoader<>(service, loader);
5 }
6
7 ...
8
9 public void reload() {
10 providers.clear();
11 lookupIterator = new LazyIterator(service, loader);
12 }
13
14 private ServiceLoader(Class<S> svc, ClassLoader cl) {
15 service = Objects.requireNonNull(svc, "Service interface cannot be null");
16 loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
17 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
18 reload();
19 }
loader方法实例了一个serviceLoader对象。在构造方法ServiceLoader(Class svc,ClassLoader cl)中做了两件事:clear provider和实例化一个延迟加载的迭代器LazyIterator。
1 // Cached providers, in instantiation order
2 private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
3
4 // The current lazy-lookup iterator
5 private LazyIterator lookupIterator;
下面看延迟迭代器两个关键方法hasNextService()和nextService()
1 private boolean hasNextService() {
2 if (nextName != null) {
3 return true;
4 }
5 if (configs == null) {
6 try {
7 String fullName = PREFIX + service.getName();
8 if (loader == null)
9 configs = ClassLoader.getSystemResources(fullName);
10 else
11 configs = loader.getResources(fullName);
12 } catch (IOException x) {
13 fail(service, "Error locating configuration files", x);
14 }
15 }
16 while ((pending == null) || !pending.hasNext()) {
17 if (!configs.hasMoreElements()) {
18 return false;
19 }
20 pending = parse(service, configs.nextElement());
21 }
22 nextName = pending.next();
23 return true;
24 }
1 private S nextService() {
2 if (!hasNextService())
3 throw new NoSuchElementException();
4 String cn = nextName;
5 nextName = null;
6 Class<?> c = null;
7 try {
8 c = Class.forName(cn, false, loader);
9 } catch (ClassNotFoundException x) {
10 fail(service,
11 "Provider " + cn + " not found");
12 }
13 if (!service.isAssignableFrom(c)) {
14 fail(service,
15 "Provider " + cn + " not a subtype");
16 }
17 try {
18 S p = service.cast(c.newInstance());
19 providers.put(cn, p);
20 return p;
21 } catch (Throwable x) {
22 fail(service,
23 "Provider " + cn + " could not be instantiated",
24 x);
25 }
26 throw new Error(); // This cannot happen
27 }
在hasNextService方法中,PREFIX = “META-INF/services/",service指的是接口名称java.sql.Driver,因此,fullName的值是META-INF/services/java.sql.Driver,这个文件由系统类加载器定位资源,解析出驱动类名称com.mysql.jdbc.Driver和com.mysql.fabric.jdbc.FabricMySQLDriver,nextName只得就是这两个类名词。
在nextService()方法中,nextName是驱动类名词,由Class.forName(cn, false, loader);加载并生成CLass对象,但不会对Class静态变量初始化。紧接着 S p = service.cast(c.newInstance()),实例化该驱动,将该实例缓存到providers中并返回对象引用。
至此,可知ServiceLoader的奥秘是,在META-INF/services目录下放置一个配置文件,文件命是服务接口的二进制名词,文件内容是服务具体实现类,每个实现类一行。java doc:
A service provider is identified by placing a provider-configuration file in the resource directory META-INF/services. The file’s name is the fully-qualified binary name of the service’s type. The file contains a list of fully-qualified binary names of concrete provider classes, one per line. Space and tab characters surrounding each name, as well as blank lines, are ignored. The comment character is ‘#’ (’\u0023’, NUMBER SIGN); on each line all characters following the first comment character are ignored. The file must be encoded in UTF-8.
ServiceLoader类的Java Doc中给了一个小例子:
Example Suppose we have a service type com.example.CodecSet which is intended to represent sets of encoder/decoder pairs for some protocol. In this case it is an abstract class with two abstract methods:
1 public abstract Encoder getEncoder(String encodingName);
2 public abstract Decoder getDecoder(String encodingName);
Each method returns an appropriate object or null if the provider does not support the given encoding. Typical providers support more than one encoding. If com.example.impl.StandardCodecs is an implementation of the CodecSet service then its jar file also contains a file named
1 META-INF/services/com.example.CodecSet
This file contains the single line: com.example.impl.StandardCodecs # Standard codecs The CodecSet class creates and saves a single service instance at initialization:
1 private static ServiceLoader<CodecSet> codecSetLoader
2 = ServiceLoader.load(CodecSet.class);
To locate an encoder for a given encoding name it defines a static factory method which iterates through the known and available providers, returning only when it has located a suitable encoder or has run out of providers.
1 public static Encoder getEncoder(String encodingName) {
2 for (CodecSet cp : codecSetLoader) {
3 Encoder enc = cp.getEncoder(encodingName);
4 if (enc != null)
5 return enc;
6 }
7 return null;
8 }
A getDecoder method is defined similarly.
JDBC的驱动加载机制
JDBC在加载驱动时候通常有一下两盒代码:
1Class.forName("com.mysql.Driver");
2DriverManager.getConnection("");
第一行将会触发这个代码块执行:
1 static {
2 try {
3 java.sql.DriverManager.registerDriver(new Driver());
4 } catch (SQLException E) {
5 throw new RuntimeException("Can't register driver!");
6 }
7 }
因此java.sql.DriverManager将会由根类加载器加载并初始化,该类的静态代码块将会执行:
1 static {
2 loadInitialDrivers();
3 println("JDBC DriverManager initialized");
4 }
loadInitialDrivers()源码如下,主要通过ServiceLoader加载服务接口为java.sql.Driver的驱动实现类,默认情况下,系统参数"jdbc.drivers"的值为空。
1 private static void loadInitialDrivers() {
2 String drivers;
3 try {
4 drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
5 public String run() {
6 return System.getProperty("jdbc.drivers");
7 }
8 });
9 } catch (Exception ex) {
10 drivers = null;
11 }
12
13 AccessController.doPrivileged(new PrivilegedAction<Void>() {
14 public Void run() {
15
16 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
17 Iterator<Driver> driversIterator = loadedDrivers.iterator();
18
19 try{
20 while(driversIterator.hasNext()) {
21 driversIterator.next();
22 }
23 } catch(Throwable t) {
24 // Do nothing
25 }
26 return null;
27 }
28 });
29
30 println("DriverManager.initialize: jdbc.drivers = " + drivers);
31
32 if (drivers == null || drivers.equals("")) {
33 return;
34 }
35 String[] driversList = drivers.split(":");
36 println("number of Drivers:" + driversList.length);
37 for (String aDriver : driversList) {
38 try {
39 println("DriverManager.Initialize: loading " + aDriver);
40 Class.forName(aDriver, true,
41 ClassLoader.getSystemClassLoader());
42 } catch (Exception ex) {
43 println("DriverManager.Initialize: load failed: " + ex);
44 }
45 }
46 }
驱动实现类被加载后,java.sql.DriverManager.registerDriver(new Driver())方法将把com.mysql.Driver实例注册到DriverManager中,然后getConnection()将会遍历驱动,获取连接。
1 private static Connection getConnection(
2 String url, java.util.Properties info, Class<?> caller) throws SQLException {
3 /*
4 * When callerCl is null, we should check the application's
5 * (which is invoking this class indirectly)
6 * classloader, so that the JDBC driver class outside rt.jar
7 * can be loaded from here.
8 */
9 ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
10 synchronized(DriverManager.class) {
11 // synchronize loading of the correct classloader.
12 if (callerCL == null) {
13 callerCL = Thread.currentThread().getContextClassLoader();
14 }
15 }
16
17 if(url == null) {
18 throw new SQLException("The url cannot be null", "08001");
19 }
20
21 println("DriverManager.getConnection(\"" + url + "\")");
22
23 // Walk through the loaded registeredDrivers attempting to make a connection.
24 // Remember the first exception that gets raised so we can reraise it.
25 SQLException reason = null;
26
27 for(DriverInfo aDriver : registeredDrivers) {
28 // If the caller does not have permission to load the driver then
29 // skip it.
30 if(isDriverAllowed(aDriver.driver, callerCL)) {
31 try {
32 println(" trying " + aDriver.driver.getClass().getName());
33 Connection con = aDriver.driver.connect(url, info);
34 if (con != null) {
35 // Success!
36 println("getConnection returning " + aDriver.driver.getClass().getName());
37 return (con);
38 }
39 } catch (SQLException ex) {
40 if (reason == null) {
41 reason = ex;
42 }
43 }
44
45 } else {
46 println(" skipping: " + aDriver.getClass().getName());
47 }
48
49 }
50
51 // if we got here nobody could connect.
52 if (reason != null) {
53 println("getConnection failed: " + reason);
54 throw reason;
55 }
56
57 println("getConnection: no suitable driver found for "+ url);
58 throw new SQLException("No suitable driver found for "+ url, "08001");
59 }
60
61
62}
该方法传入的CLass参数是调用这个方法所在类的class,callerCL是调用类的类加载器。isDriverAllowed(aDriver.driver, callerCL)用于验证先前被加载的类和当前使用的驱动类是同一个。