0
点赞
收藏
分享

微信扫一扫

Java ClassLoader、ContextClassLoader与SPI实现详解

(目录)

Java ClassLoader

ClassLoader 做什么的?

​ 众所周知, Java 或者其他运行在 JVM(java 虚拟机)上面的程序都需要最终便以为字节码,然后被 JVM加载运行,那么这个加载到虚拟机的过程就是 classloader 类加载器所干的事情.直白一点,就是 通过一个类的全限定类名称来获取描述此类的二进制字节流 的过程.

image-20231114141909272

有很多字节码加密技术就是依靠定制 ClassLoader 来实现的。先使用工具对字节码文件进行加密,运行时使用定制的 ClassLoader 先解密文件内容再加载这些解密后的字节码。

每个 Class 对象的内部都有一个 classLoader 字段来标识自己是由哪个 ClassLoader 加载的。ClassLoader 就像一个容器,里面装了很多已经加载的 Class 对象。

class Class<T> {
...
private final ClassLoader classLoader;
...
}

双亲委派模型

说到 Java 的类加载器,必不可少的就是它的双亲委派模型,从 Java 虚拟机的角度来看,只存在两种不同的类加载器:

  1. 启动类加载器(Bootstrap ClassLoader), 由 C++语言实现,是虚拟机自身的一部分.
  2. 其他的类加载器,都是由 Java 实现,在虚拟机的外部,并且全部继承自java.lang.ClassLoader

在 Java 内部,绝大部分的程序都会使用 Java 内部提供的默认加载器.

启动类加载器(Bootstrap ClassLoader)

BootstrapClassLoader 负责加载 JVM 运行时核心类,这些类位于 JAVA_HOME/lib/rt.jar 文件中,我们常用内置库 java.xxx.* 都在里面,比如 java.util.、java.io.、java.nio.、java.lang. 等等。这个 ClassLoader 比较特殊,它是由 C 代码实现的,我们将它称之为「根加载器」。

扩展类加载器(Extension ClassLoader)

ExtensionClassLoader 负责加载 JVM 扩展类比如 swing 系列、内置的 js 引擎、xml 解析器 等等,这些库名通常以 javax 开头,它们的 jar 包位于 JAVA_HOME/lib/ext/*.jar 中,有很多 jar 包。

应用程序类加载器(Application ClassLoader)

AppClassLoader 才是直接面向我们用户的加载器,它会加载 Classpath 环境变量里定义的路径中的 jar 包和目录。我们自己编写的代码以及使用的第三方 jar 包通常都是由它来加载的

当我们的 main 方法执行的时候,这第一个用户类的加载器就是 AppClassLoader

image-20231114141750796

工作流程:

  1. 收到类加载的请求
  2. 首先不会自己尝试加载此类,而是委托给父类的加载器去完成.
  3. 如果父类加载器没有,继续寻找父类加载器.
  4. 搜索了一圈,发现都找不到,然后才是自己尝试加载此类.

程序在运行过程中,遇到了一个未知的类,它会选择哪个 ClassLoader 来加载它呢?

虚拟机的策略是使用调用者 Class 对象的 ClassLoader 来加载当前未知的类

何为调用者 Class 对象?就是在遇到这个未知的类时,虚拟机肯定正在运行一个方法调用(静态方法或者实例方法),这个方法挂在哪个类上面,那这个类就是调用者 Class 对象。前面我们提到每个 Class 对象里面都有一个 classLoader 属性记录了当前的类是由谁来加载的。

因为 ClassLoader 的传递性,所有延迟加载的类都会由初始调用 main 方法的这个 ClassLoader 全全负责,它就是 AppClassLoader。

自定义加载器(Custom ClassLoader)

运用场景
  • 我们需要的类不一定存放在已经设置好的classPath下(有系统类加载器AppClassLoader加载的路径),对于自定义路径中的class类文件的加载,我们需要自己的ClassLoader
  • 有时我们不一定是从类文件中读取类,可能是从网络的输入流中读取类,这就需要做一些加密和解密操作,这就需要自己实现加载类的逻辑,当然其他的特殊处理也同样适用。
  • 可以定义类的实现机制,实现类的热部署, 如OSGi中的bundle模块就是通过实现自己的ClassLoader实现的。
ClassLoad加载class文件逻辑

ClassLoader的loadClass采用 双亲委派型实现,因为我们实现的ClassLoader都继承于java.lang.ClassLoader类,父加载器都是AppClassLoader,所以在上层逻辑中依旧要保证该模型,所以一般不覆盖loadClass函数

protected synchronized Class<?> loadClass ( String name , boolean resolve ) throws ClassNotFoundException{
//检查指定类是否被当前类加载器加载过
Class c = findLoadedClass(name);
if( c == null ){//如果没被加载过,委派给父加载器加载
try{
if( parent != null )
c = parent.loadClass(name,resolve);
else
c = findBootstrapClassOrNull(name);
}catch ( ClassNotFoundException e ){
//如果父加载器无法加载
}
if( c == null ){//父类不能加载,由当前的类加载器加载
c = findClass(name);
}
}
if( resolve ){//如果要求立即链接,那么加载完类直接链接
resolveClass();
}
//将加载过这个类对象直接返回
return c;
}

从上面的代码中,我们可以看到在父加载器不能完成加载任务时,会调用findClass(name)函数

这个就是我们自己实现的ClassLoader的查找类文件的规则,所以在继承后,我们只需要覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,而且还不会破坏双亲委托模型

注意:

不要轻易覆盖 loadClass 方法。否则可能会导致自定义加载器无法加载内置的核心类库

自定义类加载器,加载自定义路径下的class文件

覆盖findClass()这个函数,实现我们在本加载器中的查找逻辑,不会破坏双亲委托模型

package com.company;

import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;
import java.nio.ByteBuffer;

/**
* Created by liulin on 16-4-20.
*/

public class MyClassLoader extends ClassLoader {
private String classpath;

public MyClassLoader( String classpath){
this.classpath = classpath;
}

@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getClassFile( name );
byte[] classByte=null;
try {
classByte = getClassBytes(fileName);
}catch( IOException e ){
e.printStackTrace();
}
//利用自身的加载器加载类
Class retClass = defineClass( null,classByte , 0 , classByte.length);
if( retClass != null ) {
System.out.println(由我加载);
return retClass;
}
//System.out.println(非我加载);
//在classPath中找不到类文件,委托给父加载器加载,父类会返回null,因为可加载的话在
//委派的过程中就已经被加载了
return super.findClass(name);
}

/***
* 获取指定类文件的字节数组
* @param name
* @return 类文件的字节数组
* @throws IOException
*/

private byte [] getClassBytes ( String name ) throws IOException{
FileInputStream fileInput = new FileInputStream(name);
FileChannel channel = fileInput.getChannel();
ByteArrayOutputStream output = new ByteArrayOutputStream();
WritableByteChannel byteChannel = Channels.newChannel(output);
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int flag;
while ((flag = channel.read(buffer)) != -1) {
if (flag == 0) break;
//将buffer写入byteChannel
buffer.flip();
byteChannel.write(buffer);
buffer.clear();
}
}catch ( IOException e ){
System.out.println(can't read!);
throw e;
}
fileInput.close();
channel.close();
byteChannel.close();
return output.toByteArray();
}

/***
* 获取当前操作系统下的类文件合法路径
* @param name
* @return 合法的路径文件名
*/

private String getClassFile ( String name ){
//利用StringBuilder将包形式的类名转化为Unix形式的路径
StringBuilder sb = new StringBuilder(classpath);
sb.append(/)
.append ( name.replace('.','/'))
.append(.class);
return sb.toString();
}

public static void main ( String [] args ) throws ClassNotFoundException {
MyClassLoader myClassLoader = new MyClassLoader(/home/liulin/byj);
try {
myClassLoader.loadClass(java.io.InputStream);
myClassLoader.loadClass(TestServer);
myClassLoader.loadClass(noClass);
}catch ( ClassNotFoundException e ){
e.printStackTrace();
}
}
}

image-20231114144155738

从结果我们看,因为我们加载的类的父加载器是系统加载器,所以调用 双亲委托 的loadClass,会直接加载掉java.io.InputStream类,只有在加载双亲中没有的TestServer类,才会用到我们自己的findClass加载逻辑加载指定路径下的类文件

Thread.contextClassLoader(线程上下文类加载器)源码分析

阅读 Thread 的源代码,你会在它的实例字段中发现有一个字段非常特别

public class Thread implements Runnable {

// 这里省略了无关代码.....

private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals)
{
// 这里省略了无关代码....

if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
// 继承父线程的 上下文类加载器
this.contextClassLoader = parent.contextClassLoader;

// 这里省略了无关代码....
}

public Thread(Runnable target) {
init(null, target, Thread- + nextThreadNum(), 0);
}

// 这里省略了无关代码.....

}

首先 contextClassLoader 是那种需要显示使用的类加载器,如果你没有显示使用它,也就永远不会在任何地方用到它

你可以使用下面这种方式来显示使用它:

Thread.currentThread().getContextClassLoader().loadClass(name);

程序启动时的 main 线程的 contextClassLoader 就是 AppClassLoader。这意味着如果没有人工去设置,那么所有的线程的 contextClassLoader 都是 AppClassLoader

有了Thread ContextClassLoader,就可以实现父ClassLoader让子ClassLoader去完成一个类的加载任务,即父ClassLoader加载的类中,可以使用ContextClassLoader去加载其无法加载的类)。

contextClassLoader 究竟是做什么用的?

跨线程共享类,只要它们共享同一个 contextClassLoader父子线程之间会自动传递 contextClassLoader

如果不同的线程使用不同的 contextClassLoader,那么不同的线程使用的类就可以隔离开来

举例:

如果我们对业务进行划分,不同的业务使用不同的线程池,线程池内部共享同一个 contextClassLoader,线程池之间使用不同的 contextClassLoader,就可以很好的起到隔离保护的作用,避免类版本冲突。

如果我们不去定制 contextClassLoader,那么所有的线程将会默认使用 AppClassLoader,所有的类都将会是共享的。

在 SPI 中 的使用

Java 中所有涉及SPI机制的类加载基本上都是采用这种方式,最常见的就是JDBC Driver的加载

Java定义的JDBC接口位于JDK的rt.jar中(java.sql包),因此这些接口会由BootstrapClassLoader进行加载

数据库厂商提供的Driver驱动包一般由我们自己在应用程序中引入(比如位于CLASSPATH下),这已经超出了BootstrapClassLoader的加载范围,即这些驱动包中的JDBC接口的实现类无法被BootstrapClassLoader加载,只能由AppClassLoader或自定义的ClassLoader来加载

这样,SPI机制就没有办法实现

SPI 为什么需要 ThreadContextClassLoader 才能实现

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类

SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)**来加载的;

SPI的实现类是由**系统类加载器(System ClassLoader)**来加载的,依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类

要解决这个问题,就需要使用Thread Context ClassLoader,线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器

JDBC SPI 实现分析

平时是如何使用mysql获取数据库连接的:

// 加载Class到AppClassLoader(系统类加载器),然后注册驱动类
// Class.forName(com.mysql.jdbc.Driver).newInstance();
String url = jdbc:mysql://localhost:3306/testdb;
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, name, password);
12345

以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?

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

那到底是在哪一步自动注册了mysql driver的呢

重点就在DriverManager.getConnection()中。

我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块

DriverManager的静态代码块就是:

static {
loadInitialDrivers();
println(JDBC DriverManager initialized);
}

下面就查看下JDK中的DriverManager类的源码,来看看其中Thread ContextClassLoader的使用。

public class DriverManager {

// 省略无关代码......

static {
loadInitialDrivers(); // 在静态代码块中加载当前环境中的 JDBC Driver
println(JDBC DriverManager initialized);
}

private static void loadInitialDrivers() {
// 省略无关代码.....

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通过 ServiceLoader#load 方法来加载 Driver 的实现(如 MySQL、Oracle、PostgreSQL 提供的 Driver 实现)
// 即 SPI 机制
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;
}
});

println(DriverManager.initialize: jdbc.drivers = + drivers);

if (drivers == null || drivers.equals()) {
return;
}
String[] driversList = drivers.split(:);
println(number of Drivers: + driversList.length);
for (String aDriver : driversList) {
try {
println(DriverManager.Initialize: loading + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println(DriverManager.Initialize: load failed: + ex);
}
}
}

}

DriverManager类在被加载的时候就会执行通过ServiceLoader#load方法来加载数据库驱动(即Driver接口的实现)。

由于每个类都会使用加载自己的ClassLoader去加载其他的类(即它所依赖的类),因此可以简单考虑以上代码的类加载过程为:

  1. DriverManager类由BootstrapClassLoader加载,DriverManager类依赖于ServiceLoader类,因此BootstrapClassLoader也会尝试加载ServiceLoaer类,这是没有问题的;

  2. 再往下,ServiceLoader的load方法中需要加载数据库(MySQL等)驱动包中Driver接口的实现类,即ServiceLoader类依赖这些驱动包中的类;

  3. 此时如果是默认情况下,则还是由BootstrapClassLoader来加载这些类,但驱动包中的Driver接口的实现类是位于CLASSPATH下的,BootstrapClassLoader是无法加载的,这就有问题了。

因此,在ServiceLoader#load方法中实际是指明了由ContextClassLoader来加载驱动包中的类:

public final class ServiceLoader<S> implements Iterable<S> {
// 省略无关代码
public static <S> ServiceLoader<S> load(Class<S> service) {
// 需要注意的是,这里使用的是 当前线程的 ContextClassLoader 来加载实现,这也是 ContextClassLoader 为什么存在的原因。
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
}

到此总结一下:

使用TCCL,也就是说把自己加载不了的类加载到TCCL中

TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader

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

image-20231114174358816

线程上下文类加载器(TCCL)的适用场景:

  1. 当高层提供了统一接口让低层去实现,同时又要是在高层加载(或实例化)低层的类时,必须通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类。
  2. 当使用本类托管类加载,然而加载本类的ClassLoader未知时,为了隔离不同的调用者,可以取调用者各自的线程上下文类加载器代为托管。
举报

相关推荐

0 条评论