线程上下文的类加载器(setContextClassLoader)
每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是是所依赖的类),如果classX引用ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未加载)
线程上下文类加载器是从JDK1.2开始引入的,类Thread中的getContextClassLoader()与setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器
线程上下文类加载器的重要性
- SPI :(Service Provider Interface)服务提供者接口如:jdbc 是用来声明接口制定一些标准(仅仅通过双亲委托来加载类似 父加载器无法看到子类加载器(命名空间) 但是父加载器加载 如 根加载器加载一些rt.jar包里的一些内容如jdbc定义的接口,需要调外部别人实现的一些类时就无法访问到)
- 父类ClassLoader可以使用当前线程Thread.currentThread().getContextClassLoader()所指定的classLoader加载对应的类放到内存中,供父类加载器加载的类使用。这就:改变了父classLoader不能使用子ClassLoader或是其他没有直接父子关系的classLoader加载的类的情况。即改变了双亲委托模型。
- 线程上下文类加载器就是当前线程的Current Classloader。
- 在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层的加载。但是对于SPI来说,有些接口是java核心库所提供的,而java核心库是由启动类加载器来加载的,而这些接口的实现去来自于不同的jar包(厂商提供)java的启动类加载器是不会加载其他来源的jar包,这样传统的双亲委托模型就无法满足SPI的要求。
- 通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现对于接口实现类的加载
public class MyTest24 {
public static void main(String[] args) {
System.out.println(Thread.currentThread().getContextClassLoader()); //AppClassLoader
System.out.println(Thread.class.getClassLoader()); //null Thred位于lang包
}
}
如果没有设置线程上下文类加载器,该线程上下文类加载器默认设置为系统类加载器
public class MyTest25 implements Runnable {
private Thread thread;
public MyTest25() {
this.thread = new Thread(this);
thread.start();
}
@Override
public void run() {
ClassLoader classLoader = this.thread.getContextClassLoader();
this.thread.setContextClassLoader(classLoader);
System.out.println("Class: " + classLoader.getClass());
System.out.println("parent: " + classLoader.getParent().getClass());
}
public static void main(String[] args) {
new MyTest25();
}
}
/*
Class: class sun.misc.Launcher$AppClassLoader //如果没有设置线程上下文类加载器 默认设置为 系统类加载器 具体在getLauncher 讲解里
parent: class sun.misc.Launcher$ExtClassLoader //系统类加载器的父类 只是打破加载规则 并不打破包含规则
*/
线程上下文类加载器的一般使用模式
线程上下文类加载器的一般使用模式(获取->使用->还原)
//获取
ClassLoader classLoader = Thread.currentThread.getContextClassLoader();
try{
//设置要使用的类加载器
Thread.currentThread.setContextClassLoader(cls);
//使用
MyMethod();
}finally{
//如果不还原使用的就是设置的类加载器
//还原
Thread.currentThread.setContextClassLoader(classLoader);
}
上例中:
MyMethod里面则调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情 MyMethod()。
- 如果一个类由类加载器A加载,那么这个类的依赖类也是由相同的类加载器加载的(如果该依赖类之前没有被加载过的话) 如:启动类加载器扫描不到系统类加载器的内容 上下文类加载器就是为了解决这个问题
- ContextClassLoader的作用就是为了破坏Java的类加载委托机制(如上)
- 当高层提供了统一的接口让低层去实现,同时又要在高层加载(或实例化)底层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类(上下文类加载器就是为了解决父类加载器加载的类无法看到子类加载器加载的类)我们可以直接使用getClassLoader获取系统类加载器去加载对应的类为什么还要使用上下文类加载器?
1.方便,执行的任何代码都在线程中 我们可以随时取出来对应的上下文类加载器使用
2.解决 高层加载底层类问题
3.有特定的情况,当前的线程类加载器 不一定是系统类加载器 此时不能加载classpath下的.class文件
import java.sql.Driver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class MyTest26 {
public static void main(String[] args) {
//Driver驱动连接规范的一些信息
//load()等价与load(Class<S> service, ClassLoader loader)
//load(需要加载服务的class,Thread.currentThread().getContextClassLoader())
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的类加载器:"+loader.getClass().getClassLoader());
}
}
/*
为什么仅仅是Driver.class 这个接口就能找到以下两个Driver实现类
服务提供者将提供者(供应商)的程序配置文件放在资源目录META-INF/services目录**下当中。
该文件的名称(如mysql 加载java.sql.Driver文件 类加载器就可以加载这个文件里面每一行的二进制名(类名的路径 此时对应的类就会被加载))
driver: class com.alibaba.druid.proxy.DruidDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2 所有maven下载的jar都位于classpath下
driver: class com.alibaba.druid.mock.MockDriver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
driver: class com.mysql.cj.jdbc.Driver,loader: sun.misc.Launcher$AppClassLoader@18b4aac2
---
当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2 未设置默认为系统类加载器
ServiceLoader的类加载器:null --ServiceLoader位于rt.jar包下
*/
ServiceLoader(服务加载器) 源码分析 (jdk1.6之后)
1.doc官方文档ServiceLoader(服务加载器)在SPI中的重要作用分析
ServiceLoader就是用于加载提供者在特定位置META-INF/services 的服务对应服务文件名里的二进制文件名的类
ServiceLoader是一个服务提供加载者
- 已知服务的接口(抽象类的集合), 用来加载服务特定的实现(如加载JDBC接口实现类)。提供者(厂商)中的类通常实现服务者接口并继承服务本身中定义的类。服务提供者(如JDBC驱动)可以安装在Java平台的实现中以扩展名的形式,比如说以jar文件被放置到任何常用的扩展目录中。提供程序也可以通过将它们添加到应用程序的类路径(classpath)或其他特定于平台的方法 ,让其变得可用。
- 对于加载目的,服务由单一类型表示,即单个接口或抽象类。(抽象类可以是已使用,但不建议使用。)给定服务的提供者包含一个或多个具体类,用数据扩展这个服务类型和特定于提供程序的代码。提供程序类是典型的不是整个提供者本身,而是一个包含足够内容的代理决定供应商是否能够满足特定要求的信息请求和代码一起,可以根据需要创建实际的提供者。提供者类的细节往往是高度特定于服务的;没有单个类或接口可以统一它们,所以没有这样的类型在这里定义。这个设施实施的唯一要求是提供者实现类必须有一个无参数的构造函数,这样它们才能在加载期间实例化。
- 服务提供者(供应商)将提供的的程序配置文件放在驱动包资源目录META-INF/services目录下当中。该文件的名称(如mysql 加载java.sql.Driver文件 类加载器就可以加载这个文件里面每一行的二进制名(类名类名的路径 此时对应的类就会被加载))这是一个服务提供者(sun)制定的标准 是完全限定的 href = " . . / lang /服务类型二进制名。
- 该文件包含一个完全限定的二进制名的具体提供程序列表,每行放置一个(类的二进制名)。空格和每个字符周围的制表符name和空行将被忽略。注释字符“#”;在每行第一个注释字符后面的所有字符将被忽略。文件名必须用UTF-8编码。(这句话的意思 服务提供者必须要一种方式告诉JDK我的提供者(厂商)具体的类是如何定义在 META-INF/services目录下的,而且文件名是服务类型的名字(给父类使用类二进制名)在这个文件中就可以一行指定一个提供者的类名(实现类) 需要上下文加载器加载的类 #是作为注释)
- 如果在多个配置文件中出现了同一个具体提供者类的名字的话,或在相同的配置文件中名字出现了一次以上,重复项将被忽略。配置文件特定的提供者不需要在相同的jar文件或其他发行版中单独作为提供者本身。提供者必须是可从相同的类加载器,与最初被定位配置文件相同的类加载器加载出来;注意,这个并不是必要的。
- 提供程序是按需定位和实例化的。一个服务加载器维护一个已加载的提供者缓存。每次调用{@link #iterator}方法都会返回一个迭代器,它首先返回缓存中的所有元素实例化顺序,然后延迟定位和实例化任何剩余的提供程序,依次将每个提供程序添加到缓存中。可以清除缓存通过{@link #reload reload}方法。(提供者当中的类都是延迟实例化和加载的 换句话说就是什么时候使用就什么时候加载,在serviceLoader中存在缓存,缓存已经加载过的服务提供者提供的类)缓存存储位置
他是按照实例化顺序添加缓存的
- 服务加载程序总是在调用者的安全上下文中执行。受信任的系统代码通常应该调用这个类中的方法,并且他们返回的迭代器的方法,从一个特权安全上下文。
- 并不是线程安全的一个类
- 除非另有说明,否则将null参数传递给any方法将导致抛出{@link NullPointerException}。
- 假设我们有一个服务类型com.example.CodecSet (提供者类型与sci规范对应) 这是用于表示某些协议的编码器/解码器对的集合。在这种情况下,它是一个抽象类,有两个抽象方法:
public abstract Encoder getEncoder(String encodingName);//获取解码器
public abstract Decoder getDecoder(String encodingName);//获取编码器
- 每个方法返回一个适当的对象或null(如果提供程序)不支持给定的编码。典型的提供者支持不止一个编码。
- 如果com.example.impl是com.example.CodecSet(是java定义的一个规范接口的)的具体实现类的话,那么它的jar文件也包含一个名为的 META-INF/services/com.example.CodecSet的文件(对应提供者也必须在指定位置有一个同样的接口名的文件)
- 这个文件会包含如下这一行com.example.impl.StandardCodecs (类似mysql的驱动)
- CodecSet类创建并保存一个服务实例 如上如果是两个就是加载保存两个实例
private static ServiceLoader<CodecSet>codecSetLoader= ServiceLoader.load(CodecSet.class);
此时load(CodecSet.class); 加载的就是/com.example.CodecSet的文件中对应的二进制名的类的实例
(个人理解就是你要对接java规范中那个服务CodecSet就是对应的服务实例)
- The type of the service to be loaded by this loader (加载的泛型就是要加载的服务的类型)
ServiceLoader.load(String service)源码 解析
public static <S> ServiceLoader<S> load(Class<S> service) {
//ServiceLoader在核心库中使用根加载器无法加载对应实现类
//所以的获取当前上下文类加载器(未设置 在launcher中设置为系统类加载器
//基本类加载都会加载Launcher类)
//核心代码
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;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//拼接对应的加载路径 PREFIX 在serviceLoader中定义了"META-INF/services/";
//SPI规定了 所以加载的就是fullName = 其目录下 +服务接口文件名
String fullName = PREFIX + service.getName();
if (loader == null)
//此时加载指定文件里的类
configs = ClassLoader.getSystemResources(fullName);
else
//没有加载器系统路径
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//迭代扫描目录
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析文件
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
//文件中对应的每一行二进制类名
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//使用loader加载对应类名并不实例化
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//实例化
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
//下一行 hasNext next remove 用于迭代
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
//当前行数据
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);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
当设置上下文为扩展类加载器,此时驱动在当前目录下 此时就无法找到对应的jar 此时驱动里的实现类就无法加载
public class MyTest26 {
public static void main(String[] args) {
//设置上下文为扩展类加载器
Thread.currentThread().setContextClassLoader(MyTest23.class.getClassLoader().getParent());
//Driver驱动连接规范的一些信息
//load()等价与load(Class<S> service, ClassLoader loader)
//load(需要加载服务的class,Thread.currentThread().getContextClassLoader())
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的类加载器:" + loader.getClass().getClassLoader());
}
}
打印
当前线程上下文类加载器: sun.misc.Launcher$ExtClassLoader@1eb44e46
ServiceLoader的类加载器:null
问题分析P31
public class MyTest27 {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/mytestdb","username","password");
}
}
注意的点: 此时判断是否是同一个类加载器加载的(命名空间) 避免找不到对应的类