类加载器的约束——双亲委派,以及打破双亲委派

什么是类加载器?

整个类加载过程的任务非常繁重,虽然任务中,但是总要有人干。类加载器做的就是上面5个步骤的事。(加载,验证,准备,解析,初始化)。

JDK提供的类加载器

Bootstrap ClassLoader(启动)

这是类加载器中的扛把子,任何类的加载行为,都要经过它。它的作用是加载核心类库,也就是rt.jar.resources.jar,charsets.jar等。当然这些jar包的路径是可以指定的,-Xbootclasspath参数可以完成指定操作。

这个加载器是C++编写的,随着JVM启动(也就是核心类都在程序启动时就已经加载完毕了,其他的类则需要根据不同的时机加载)。

Extention ClassLoader(延伸)

扩展类加载器,主要用于加载jdk的lib/ext目录下的jar包和.class文件。同样的,通过系统变量java.ext.dirs可以指定这个目录。这个加载器是个Java类,继承自URLClassLoader。

Application ClassLoader(应用)

这是我们写的Java类的默认加载器。有时候也叫作System ClassLoader。一般用来加载classpath下的其他所有jar包和.class文件。我们写的代码首先尝试使用这个类加载器进行加载。

Custom ClassLoader(自定义)

自定义类加载器,支持一些个性化的扩展功能。

如何确定类的唯一性

如果在我们的项目里写一个java.lang包,然后改写一些String类的行为。编译后,发现并不能生效。JRE的类当然不能这么轻易被覆盖,否则会被别有用心的人利用,这就太危险了。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在一个Java虚拟机中的唯一性。每一个类加载器都拥有一个独立的类名称空间。这句话可以表达的更通俗一点:比较两个类是否”相等”,只有在这两个类是由同一个类加载器的前提下才有意义。否则即使这两个类源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

这里所指的”相等”,包括代表类的Class对象的equals()方法,isAssignableFrom()方法,isInstance()方法的返回结果,也包括使用instanceof关键字做对象所属关系判定等情况。

如何确定类的唯一性?

类全限定名+对应的ClassLoader

那么虚拟机是如何选择一个类所对应的类加载器呢?

类加载器的规则之双亲委派机制

双亲委派模型要求除了顶层的启动类加载器外,其余的类加载都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承的关系来实现,而是都使用组合关系(一个类持有另一个类)来复用类加载器的代码。

使用双亲委派模型来组织类加载器之间的关系,有一个显而易见的好处就是Java类随着它的类加载器具备了一种带有优先级的层次关系。例如类java.lang.Object,它存放在rt.jar中。无论哪一个类加载要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载。因此Object类在程序的各种类加载器环境中都是一个类,相反,如果没有双亲委派模型,由个各类加载器自行去加载的话,如果用户自己编写了一个称为java.lang.Object的类,并放在程序的ClassPath中,那系统中将会出现多个不同的Object类,Java类型体系中最基础的行为也就无法保证,应用程序也将变得一片混乱。

总结:双亲委派模型让类存在一种优先级顺序,子类加载器负责的类永远无法干预父类加载器负责的类,从而提升系统安全性。

image.png

通过源码理解类加载器

我们可以翻阅JDK代码的ClassLoader类loadClass方法,来看一下具体的加载过程。和我们描述的一样。它首先使用parent尝试进行类加载,parent失败后才轮到自己。同时,我们也注意到,这个方法是可以被覆盖的。也就是双亲委派机制不一定生效。

parent.loadClass代表向上传递这个类。图片1.png

形参name代表类全限定命名。

loadClass方法用于根据类的全限定名寻找加载类的类加载器。由源码可知,当查询到当前类加载器有父类加载器时,会先切换到父类加载器中。用递归的方式找到最顶层的父类加载器(Bootstrap ClassLoader)。再由最顶层的父类加载器往下依次判断是否可以加载这个类。每个类最终都会寻找到属于自己的,唯一的类加载器。

类加载器的层级关系

图片3.png

通过本段代码,可以明白从层级的角度看类加载器,可以得到Bootstrap ClassLoader>Extention ClassLoader>Application ClassLoader的结论。

并且BootStrapClassLoader类加载器不是通过JVM加载的,底层是C++实现的。

打破双亲委派

我们之前提到,双亲委派是为了让实现分层加载,但为什么这里我们又要讲如何打破他呢?在此,我们用两个例子来说明双亲委派除了让类的与类之间产生隔离性,却也造成了造成了局限性。

Tomcat类加载机制

Tomcat通过war包进行应用发布,它其实违反了双亲委派原则。简单看一下tomcat类加载器的层次结构。

图片4.png

对于一些需要加载的基础类,会有一个叫做WebAppClassLoader的类加载器优先加载。等它加载不到的时候再交给上层的ClassLoader进行加载。(这个顺序和JVM的双亲委派是反的)。这个加载器用来隔绝不同应用的.class文件。比如同一个项目的不同版本,他们是互相没有影响的。如果依然按照双亲委派模式,那么优先加载的项目的类会成功加载。而后续的类则无法加载而报错。

那么如何在JVM中,运行不兼容的两个版本的项目。当然需要自定义类加载器才能完成。

那么tomcat是如何打破双亲委派的呢。可以看图中WebAppClassLoader,它加载自己目录下的.class文件时,并不会传递给父类的加载器。从而实现了隔离。

图片5.png

图片6.png

可见delegate默认是false,所以不会走到使用parent父类加载器的模块中。

在Tomcat中,可以使用SharedClassLoader所加载的类实现共享,使用WebAppClassLoader所加载的类实现分离。

可如果自己写一个ArrayList放在WEB-INF/lib目录下,依旧不会加载。因为JVM已经加载好的类依旧遵循双亲委派。

SPI机制

JDBC

Java中有一个SPI机制,全称是Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

这个说法可能比较晦涩。我们用常用的数据库驱动加载来说。在使用JDBC写程序之前,通常会调用下面这行代码,用于加载所需要加载的驱动类。

Class.forName(“com.mysql.cj.jdbc.Driver”)

这只是一种初始化模式,通过static代码块显示声明了驱动对象,然后把这些信息,保存到底层的一个List中,这是一个接口编程的思路。

但是你会发现,即时删掉了Class.forName这行代码,也能加载到正确的驱动类。什么都不需要做,非常的神奇。这是怎么做到的呢?

图片7.png

图片8.png

MySql驱动代理,就是在这里实现的。

路径:mysql-connector-java-8.0.11.jar/META-INF/services/java.sql.Driver

里面的内容就是:com.mysql.cj.jdbc.Driver

图片10.png

通过在 META-INF/services 目录下,创建一个以接口全限定名为命名的文件(内容为实现类的全限定名),即可自动加载这一种实现,这就是 SPI。下面详细解释一下。

双亲委派很好地解决了各类加载器协作时基础类型的一致性问题(越基础的类由越上层的加载器加载)。基础类型之所以被称为”基础”。是因为它们总是作为被用户代码继承,调用的API存在,但是程序设计往往没有绝对不变的完美规则,如果基础类型又要调用回用户的代码,那该怎么办呢?

这并非不可能出现的事,一个典型的例子便是JNDI服务,JNDI现在已经是Java的标准服务,它的代码由启动类加载器来完成加载(在JDK1.3时加入到了rt.jar的),肯定数据Java很基础的类型了。但是JNDI存在的目的就是对资源进行查找和集中管理,它需要调用由其他厂商实现并部署在应用程序的ClassPath下的JNDI服务提供者接口(Service Provider Interface,SPI)的代码。现在问题来了,启动类加载器是绝不可能认识,加载这些类加载器的。现在明确了我们的目标:我们要加载这些类就需要使用负责加载这些类的类加载器。那么如何获取呢?

为了解决这个困境,Java团队师引入了一个不太优雅的设计:线程上下文类加载器(我们肯定是在自己的代码调用父类加载器的代码,因此上下文的类加载器就是我们的application类加载器) 。这个类加载器可以通过java.lang.Thread类的setContext-ClassLoader()方法进行设置。如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序,那这个类加载器默认就是应用程序类加载器。

有了上下文类加载器,程序就用了”舞弊”的能力。JNDI服务使用这个线程上下文类加载器去加载所需的SPI服务代码,这是一种父类加载器去请求子类加载器完成类加载的行为。这种行为实际上是打通了双亲委派的层次结构来逆向使用类加载器,也违背了其一般原则性。但这也是无可奈何的事,Java中设计SPI的加载基本上都采用这种方式来完成。

图片9.png

这种方式同样打破了双亲委派。

从JNDI源码,理解SPI机制

DriverManager类和ServiceLoader类都是属于rt.jar的。他们的类加载器是Bootstrap ClassLoader,而具体操作的数据库驱动却来自于业务代码。这个启动类加载器是无法加载的。此时该怎么办呢?

Java本身的管理服务JNDI,是放置在rt.jar中,由启动类加载器加载的,以对数据库管理JDBC为例。

Java给数据库操作提供了一个Dirver接口:

图片11.png

图片12.png

此处省略了大部分代码,但我们可以发现,使用数据库驱动前必须要在BootstrapClassLoader的DriverManager中使用registerDriver()注册才能正常使用。

不破坏双亲委派(不适用于JNDI服务)

我们看一下mysql的驱动是如何被加载的。

图片13.png

核心就是Class.forName()触发了mysql驱动的加载。我们看下mysql对Driver的对接。

图片14.png

可以看到,Class.forName()其实就是触发了静态代码块,然后向DriverManager中注册了一个mysql的Driver实现。

这个时候我们通过DriverManager去获取connection的时候,只要建立当前所有Driver实现,然后选择一个建立连接就可以了。

破坏双亲委派模型的情况

在JDBC4.0以后,开始支持使用spi的方式来注册这个Driver,具体的做法就是在mysql的jar包中的META-INF/services/java.sql.Driver文件中指明当前使用的Driver是哪个,然后使用的时候就直接进行连接就好了。

图片15.png

这里我们发现是直接获取连接,省略了Class.forName()的注册过程。

现在我们分析一下使用了这种spi服务的模式根本过程是怎样的:

1. 从META-INF/services/java.sql.Driver文件中获取具体的实现类名“com.mysql.jdbc.Driver”。

2. 加载这个类,这里肯定只能用class.forName (“com.mysql.jdbc.Driver”)来加载

那么问题来了,Class.forName()加载用的是调用者的ClassLoader,这个调用者DriverManager是在rt.jar中的,ClassLoader是启动类加载器,而com.mysql.jdbc.Driver”肯定不在<JAVA_HOME>/lib下,所以肯定无法加载mysql中的这个类。这就是双亲委派的局限性了,父类加载器无法加载子类加载器路径中的类。

那么问题怎么解决呢?按照目前的情况来分析,这个mysql的driver只能应用类加载器能加载,那么我们只要在启动类加载器的方法获取应用类加载器。然后通过它去加载就可以了。这就是所谓的线程上下文加载器。(线程是main线程,main线程所处的上下文是应用类加载器加载的)、

线程上下文类加载器可以通过Thread.setContextClassLoader()方法设置。如果不特殊设置会从父类继承。一般默认使用的是应用类程序类加载器

很明显,线程上下文类加载器父类加载器能通过调用子类加载器来加载,这就打破了双亲委派模型的原则。

现在我们来看一下DriverManager是如何通过上下文类加载器去加载第三方jar包中的Driver类的。

图片23.png

使用时,直接调用DriverManager.getConn()方法自然会触发静态代码块的执行。开始加载驱动。

然后ServiceLoader.load()的具体实现。

图片22.png

可以看到核心就是拿到线程上下文类加载器,然后构造了一个ServiceLoader,后续的具体查找过程就不再分析,只要找到ServiceLoader已经拿到了线程上下文类加载器即可。

接下我,DriverManager的LoadInitialDrivers()方法中有一句driversIterator.next();它的具体实现如下:

图片16.png

看到了么看到了么?看到我们熟悉的Class.forName()方法了。其实DriverManager.getConnection()方法本子上就是先使用bootstrap类加载器中的DriverManager类。再在BootStrap加载器中通过上下文环境(毕竟我们写代码肯定都是在application或者更下级的自定义类加载器中加载的)获取application类加载器。兜了个大圈子,目标很清楚,就是实现在父类加载器中能合理的调用Class.forName()方法!

至此,我们成功的通过线程上下文类加载器拿到了应用程序类加载器(,同时我们也查找到了厂商在子级jar包中注册的驱动具体实现类名,这样我们就可以成功的在rt.jar包中的DriverManager中成功的加载了放在第三方应用程序包中的类了。

总结

纵观整个mysql的驱动加载过程:

1. 获取线程上下文类加载器,从而也就获得了应用程序类加载器(也可能是自定义的类加载器)。

2. 从META-INF/services/java.sql.Driver文件中获取具体的实现类名”com.mysql.jdbc.Driver”(ServiceLoader.load(Driver.class))

3. 用过线程上下文加载器去加载这个Driver类,从而避开了双亲委派模型的弊端。

很明显,mysql驱动采用这种spi服务确确实实破坏了双亲委派模型。毕竟做到了父类加载器加载了子级路径中的类。

手写破坏双亲委派

其实在上述案例中大概讲述了,我们阐述了mysql打破双亲委派是如何做到的,那么我们是不是可以自己尝试一下打破一次双亲委派呢?

首先,我们需要先创建一个只会被Extention类加载器加载的类ShuangqinTest。

图片17.png

把这个类打包成jar包,放到jdk\jre\lib\ext目录下,给Extention类加载器识别。

图片20.png

此时,项目目录中也引入了这个jar文件,如图。

图片18.png

这样ShuangqinTest类就只会被Extention类加载器加载啦!

在本类中定义Test方法,把application加载器加载的类路径写死。locadExtClass方法与Test类相似,只是把类的路径改为参数。

接着通过上下文Thread.currentThread().getContextClassLoader()获取application类加载器。之后再利用反射,就可以在Extention类加载器加载的类中调用application类加载器加载的类中的方法啦。具体运行结过来看看吧。

图片22.png

图片23.png

大家可以自己试一试,加深一下打破类加载器方案的思路。

OSGI(了解)

OSGI曾经非常流行,Eclipse就是使用OSGI作为插件系统的基础。OSGI是服务平台的规范,立志用于需要长时间运行,动态更新和对环境破坏最小的系统。

OSGI规范定义了很多关于包生命周期,以及基础架构和绑定包的交互方式。这些规则,通过使用特殊Java类加载器来强制执行,比较霸道。

比如一般Java应用中,classpath中所有类都对其他类可见,这是毋庸置疑的。但是OSGI类加载器基于OSGI规范和每个绑定包的manifes.mf文件中指定的选项,来限制这些类的交互,这就让编程风格变得非常怪异。但我们不难想象,这种与直觉相违背的加载方式,这些都是由专门的类加载器来实现的。

随着JPMS的发展(JDK9引入的,旨在为Java SE平台设计,实现一个标准的模块系统)

类与类关系之组合与继承区别和联系

组合

在当前类中声明另一个类的对象作为其成员变量即可。

图片24.png

优点:

1. 耦合度低。引用对象内部细节改变不会影响当前对象的使用。

2. 可以动态绑定。当前对象可以再运行时动态绑定所包含的对象。

缺点

容易产生过多的对象。

继承

继承就是子类继承父类的特征和行为,使得子类对象具有父类的实例域和方法,或子类从父类继承方法,使得子类具有和父类相同的行为。

继承的重点不在于”为新的类提供方法”,而是表现新类和基类之间的关系--”新类是现有类的一种形式”。

图片25.png

继承的特性:

1. 子类拥有父类的非private的属性和方法。

2. 子类可以拥有自己的属性和方法,对父类进行扩展。

3. 子类可以用自己的方式实现父类的方法(方法重写)。

4. 单继承,一个子类只有一个父类。

5. 多重继承,即一个子类继承一个父类,而这个父类还可以继承它的父类。

6. 提高了类之间的耦合性-----继承的缺点(组合可以降低耦合度)

7. 破坏封装,为了复用代码可能使子类具有不该具有的功能---继承的缺点。

总结:如果是仅仅为了复用类的方法,推荐优先考虑组合的方式。

 

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/109803737