【JVM类加载】线程上下文加载器分析ServiceLoader.load源码详解
nanshan 2024-11-10 10:11 6 浏览 0 评论
线程上下文的类加载器(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");
}
}
注意的点: 此时判断是否是同一个类加载器加载的(命名空间) 避免找不到对应的类
相关推荐
- F5负载均衡器如何通过irules实现应用的灵活转发?
-
F5是非常强大的商业负载均衡器。除了处理性能强劲,以及高稳定性之外,F5还可以通过irules编写强大灵活的转发规则,实现web业务的灵活应用。irules是基于TCL语法的,每个iRules必须包含...
- 映射域名到NAS
-
前面介绍已经将域名映射到家庭路由器上,现在只需要在路由器上设置一下端口转发即可。假设NAS在内网的IP是192.168.1.100,NAS管理端口2000.你的域名是www.xxx.com,配置外部端...
- 转发(Forward)和重定向(Redirect)的区别
-
转发是服务器行为,重定向是客户端行为。转发(Forward)通过RequestDispatcher对象的forward(HttpServletRequestrequest,HttpServletRe...
- SpringBoot应用中使用拦截器实现路由转发
-
1、背景项目中有一个SpringBoot开发的微服务,经过业务多年的演进,代码已经累积到令人恐怖的规模,亟需重构,将之拆解成多个微服务。该微服务的接口庞大,调用关系非常复杂,且实施重构的人员大部分不是...
- 公司想搭建个网站,网站如何进行域名解析?
-
域名解析是将域名指向网站空间IP,让人们通过注册的域名可以方便地访问到网站的一种服务。IP地址是网络上标识站点的数字地址,为方便记忆,采用域名来代替IP地址标识站点地址。域名解析就是域名到IP地址的转...
- 域名和IP地址什么关系?如何通过域名解析IP?
-
一般情况下,访客通过域名和IP地址都能访问到网站,那么两者之间有什么关系吗?本文中科三方针对域名和IP地址的关系和区别,以及如何实现域名与IP的绑定做下介绍。域名与IP地址之间的关系IP地址是计算机的...
- 分享网站域名301重定向的知识
-
网站域名做301重定向操作时,一般需要由专业的技术来协助完成,如果用户自己在维护,可以按照相应的说明进行操作。好了,下面说说重点,域名301重定向的操作步骤。首先,根据HTTP协议,在客户端向服务器发...
- NAS外网到底安全吗?一文看懂HTTP/HTTPS和SSL证书
-
本内容来源于@什么值得买APP,观点仅代表作者本人|作者:可爱的小cherry搭好了NAS,但是不懂做好网络加密,那么隐私泄露也会随时发生!大家好,这里是Cherry,喜爱折腾、玩数码,热衷于分享数...
- ForwardEmail免费、开源、加密的邮件转发服务
-
ForwardEmail是一款免费、加密和开源的邮件转发服务,设置简单只需4步即可正常使用,通过测试来看也要比ImprovMX好得多,转发近乎秒到且未进入垃圾箱(仅以Mailbox.org发送、Out...
- 使用CloudFlare进行域名重定向
-
当网站变更域名的时候,经常会使用域名重定向的方式,将老域名指向到新域名,这通常叫做:URL转发(URLFORWARDING),善于使用URL转发,对SEO来说非常有用,因为用这种方式能明确告知搜索引...
- 要将端口5002和5003通过Nginx代理到一个域名上的操作笔记
-
要将端口5002和5003通过Nginx代理到域名www.4rvi.cn的不同路径下,请按照以下步骤配置Nginx:步骤说明创建或编辑Nginx配置文件通常配置文件位于/etc/nginx/sites...
- SEO浅谈:网站域名重定向的三种方式
-
在大多数情况下,我们输入网站访问网站的时候,很难发现www.***.com和***.com的区别,因为一般的网站主,都会把这两个域名指向到同一网站。但是对于网站运营和优化来说,www.***.com和...
- 花生壳出现诊断域名与转发服务器ip不一致的解决办法
-
出现诊断域名与转发服务器ip不一致您可以:1、更改客户端所处主机的drs为223.5.5.5备用dns为119.29.29.29;2、在windows上进入命令提示符输入ipconfig/flush...
- 涨知识了!带你认识什么是域名
-
1、什么是域名从技术角度来看,域名是在Internet上解决IP地址对应的一种方法。一个完整的域名由两个或两个以上部分组成,各部分之间用英文的句号“.”来分隔。如“abc.com”。其中“com”称...
- 域名被跳转到其他网站是怎么回事
-
当你输入域名时被跳转到另一个网站,这可能是由几种原因造成的:一、域名可能配置了域名转发服务。无论何时有人访问域名,比如.com、.top等,都会自动重定向到另一个指定的URL,这通常是在域名注册商设...
你 发表评论:
欢迎- 一周热门
-
-
爱折腾的特斯拉车主必看!手把手教你TESLAMATE的备份和恢复
-
如何在安装前及安装后修改黑群晖的Mac地址和Sn系列号
-
[常用工具] OpenCV_contrib库在windows下编译使用指南
-
WindowsServer2022|配置NTP服务器的命令
-
Ubuntu系统Daphne + Nginx + supervisor部署Django项目
-
WIN11 安装配置 linux 子系统 Ubuntu 图形界面 桌面系统
-
解决Linux终端中“-bash: nano: command not found”问题
-
NBA 2K25虚拟内存不足/爆内存/内存占用100% 一文速解
-
Linux 中的文件描述符是什么?(linux 打开文件表 文件描述符)
-
K3s禁用Service Load Balancer,解决获取浏览器IP不正确问题
-
- 最近发表
- 标签列表
-
- linux 查询端口号 (58)
- docker映射容器目录到宿主机 (66)
- 杀端口 (60)
- yum更换阿里源 (62)
- internet explorer 增强的安全配置已启用 (65)
- linux自动挂载 (56)
- 禁用selinux (55)
- sysv-rc-conf (69)
- ubuntu防火墙状态查看 (64)
- windows server 2022激活密钥 (56)
- 无法与服务器建立安全连接是什么意思 (74)
- 443/80端口被占用怎么解决 (56)
- ping无法访问目标主机怎么解决 (58)
- fdatasync (59)
- 405 not allowed (56)
- 免备案虚拟主机zxhost (55)
- linux根据pid查看进程 (60)
- dhcp工具 (62)
- mysql 1045 (57)
- 宝塔远程工具 (56)
- ssh服务器拒绝了密码 请再试一次 (56)
- ubuntu卸载docker (56)
- linux查看nginx状态 (63)
- tomcat 乱码 (76)
- 2008r2激活序列号 (65)