WatchDogs

Knowledge of Backend Development

0%

双亲委派模型的破坏

SPI(JDBC)对双亲委派模型的破坏

什么是SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。

SPI和API的使用场景
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。

SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

这里我们就用jdbc对双亲委派模型的破坏来展示

JDBC之所以要破坏双亲委派模式是因为,JDBC的核心在rt.jar中由启动类加载器加载,而其实现则在各厂商实现的的jar包中,根据类加载机制,若A类调用B类,则B类由A类的加载器加载,也就是说启动类加载器要加载jar包下的类,我们都知道这是不可能的,启动类加载器负责加载$JAVA_HOME中jre/lib/rt.jar里所有的class,那么JDBC是如何加载这些Driver实现类的?

我们在

1
DirverManager.getConnection()

之前,必须先将Driver实例注册到DriverManager里面,注册Driver则需要执行

1
DriverManager.registerDriver(new Driver());

我们再看看com.mysql.cj.Driver中的静态代码块

1
2
3
4
5
6
7
8
9
10
11
12
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.cj.Driver加载完成并初始化之后,mysql的Driver就会注册到DriverManager中。

在刚学JDBC的时候,会要求写这样的代码:

1
2
3
4
//1.加载驱动程序
Class.forName("com.mysql.jdbc.Driver");
//2. 获得数据库连接
Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);

但是后面发现,只要导入了mysql的jar驱动包,直接去掉Class.forName这一句也可以运行。

这是为什么呢?我们看看DriverManager中的静态代码块

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

原来DriverManager在初始化时,执行loadInitialDrivers()将驱动注册到类里面。如果没有loadInitialDrivers(),但是自己写的代码里面有Class.forName()也不会有问题。但是如果没有Class.forName(),就必须依靠loadInitialDrivers(),我们这里讨论没有Class.forName()的情况。

如果是你,你会怎么写loadInitialDrivers()?

是这样吗?

1
2
3
4
5
6
7
8
9
10
private static void loadInitialDrivers() {
...
registerDriver(new com.mysql.cj.Driver());
...

//还是这样?
...
Class.forName("com.mysql.cj.Driver");
...
}

上面这两种方式是常规的,遵循双亲委派模型的加载方法。根据类加载机制,若A类调用B类,则B类由A类的加载器加载而这里会出现异常。

我们首先要知道new ClassName()和 Class.forName的原理

首先是new,new 的时候如果发现这个类没有加载,那么,jvm就会使用 加载 这段代码所在的类 所使用的类加载器 加载被new的类
读不懂的可以看

1
2
3
4
5
6
7
8
9
10
public class A {
void test() {
new B();
//相当于
//A.class.getClassLoader().loadClass("B").newInstance();

Class.forName("C");
//相当于A.class.getClassLoader().loadClass("C");然后再执行初始化块
}
}

所以,如果要在loadInitialDrivers()里面加载com.mysql.cj.Driver,同时遵守双亲委派模型,那就必须用C/C++实现的启动类加载器。因为加载DriverManager的类就是启动类加载器(DriverManager在rt.jar中)。而启动类加载器只能加载rt.jar,所以,最终会加载失败。

注意:在这里,启动类加载器不会调用扩展类加载器,应用程序类加载器。我们所学习的双亲委派模型的概念,是从应用程序类加载器的角度解释的:

双亲委派的意思是如果一个类加载器需要加载类,那么首先它会把这个类请求委派给父-类加载器去完成,每一层都是如此。一直递归到顶层,当父-加载器无法完成这个请求时,子类才会尝试去加载。

而最后一句当父-加载器无法完成这个请求时,子类才会尝试去加载。会造成误解。如果我们请求应用程序类加载器加载,递归调用父加载器加载失败,应用程序类加载器会尝试加载,如果加载再失败,就直接抛出错误,不会调用自定义类加载器。

如果我们请求应用程序类加载器加载,则会在JVM内存中的虚拟机栈入栈,然后应用程序类加载器调用扩展类加载器,在JVM内存中的虚拟机栈入栈,扩展类加载器调用启动类加载器,在本地方法栈入栈。启动类加载器加载失败,本地方法栈出栈,回到扩展类加载器。扩展类加载器加载失败,虚拟机栈出栈,应用程序类加载器尝试加载,加载失败。应用程序类加载器出栈。此时,有关类加载器和双亲委派模型的栈已空,则返回加载失败。因为自定义类加载器没有入栈,即一开始没有请求自定义类加载器,则不会调用自定义类加载器。而双亲委派类加载就结束在应用程序类加载器。因为我们一开始请求的就是应用程序类加载器

换句话说,当父-加载器无法完成这个请求时,子类才会尝试去加载。通过栈和递归使子类去尝试加载的,而不是主动请求子类去加载的

因为请求类加载器的递归入口是应用程序类加载器,则应用程序类加载器加载失败时,递归就结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
........
try {
if (parent != null) {
//父加载器加载
c = parent.loadClass(name, false);
} else {
//父加载器为启动类加载器时,使用此方法内部调用的native方法调用启动类加载器
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// 自己尝试加载
long t1 = System.nanoTime();
c = findClass(name);
...
}
........
}

如果 加载 这段代码所在的类 所使用的类加载器 是启动类加载器,则直接调用findBootstrapClassOrNull(name),若无法加载类,则整个类加载过程结束。因为进入请求的类加载器是启动类加载器,则递归入口就是启动类加载器,而递归就将结束在启动类加载器尝试加载完成时。不会请求扩展类加载器加载。

所以,在在loadInitialDrivers()里面加载com.mysql.cj.Driver,同时遵守双亲委派模型,只能使加载 DriverManager 所使用的类加载器 ,即启动类加载器,而启动类加载器必然无法加载com.mysql.cj.Driver,因为他不在rt.jar中,则com.mysql.cj.Driver必然加载失败。

由上所知,难以理解遵守双亲委派模型,是难以理解以下两点:

  • 根据类加载机制,若A类调用B类,则B类由A类的加载器加载
  • 最先被请求的类加载器无法主动调用其子类加载器

那么,如何破坏双亲委派模型呢?

我们先看看loadInitialDrivers();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static void loadInitialDrivers() {
......
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;
}
......
});

首先我们看这里:

1
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这里的作用是,迭代所有的jar包中的 META-INF/services/java.sql.Driver 文件,对于每一个文件中的Driver实现类的全限定名,放入迭代器。

让我们看看mysql-connector jar包中的META-INF/services/java.sql.Driver

1
com.mysql.cj.jdbc.Driver

意思就是,ServiceLoader已经记住这个全限定名,待会在迭代的过程中加载。

那用什么加载器呢?

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

java.lang.Thread.getContextClassLoader() 方法返回该线程的上下文类加载器。上下文ClassLoader是在这个线程加载类和资源在运行时使用的代码的线程的创建者提供的。此方法大部分时候都返回应用程序类加载器,除非在这个线程中调用了setContextClassLoader()。具体可以自行了解。

这里获得线程上下文的ClassLoader,最终将被Class.forName使用,用该加载器来加载在我们项目下的com.mysql.jdbc.Driver。

然后我们进入迭代语句

1
driversIterator.next();

里面是这样的

1
2
3
4
5
6
7
8
9
10
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

我们再看看nextService();

1
2
3
4
5
6
7
8
9
10
11
      private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);

......
}

这里的

1
c = Class.forName(cn, false, loader);

就是调用类加载器的入口。这里的loader正是刚才的ContextClassLoader,线程上下文类加载器,即应用程序类加载器。

当然,双亲委派的委派这个流程还是要走完的:
应用程序类加载器委托->扩展类加载器委托->启动类加载器尝试失败->扩展类加载器尝试失败->应用程序类加载器加载成功

最后执行的是应用程序类加载器的findClass()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
........
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} ......

if (c == null) {
......
c = findClass(name); // <-------------------在这里
...
}
........
}

其实一开始说破坏了双亲委派模型,其实说破坏吧,但是

1
c = Class.forName(cn, false, loader);

这里又好像没破坏。

但是要说破坏吧,破坏的其实是这一条规则:
根据类加载机制,若A类调用B类,则B类由A类的加载器加载

既然大家都说破坏了,那我们就记住,jdbc(java spi) 破坏双亲委派模型是因为:
加载实现类时使用了线程上下文类加载器,而不是 加载 DriverManager 的类加载器