查看: 87|回复: 6

java ContextClassLoader (线程上下文类加载器)

[复制链接]

2

主题

4

帖子

8

积分

新手上路

Rank: 1

积分
8
发表于 2023-7-15 19:18:29 | 显示全部楼层 |阅读模式
一、基础知识点

知识点1: 每个ClassLoader都只能加载自己所绑定目录下的资源;
知识点2: 加载资源时的ClassLoader可以有多种选择:
   1. 系统类加载器SystemClassLoader,可通过ClassLoader.getSystemClassLoader()获得;
   2. 当前ClassLoader:加载了当前类的ClassLoader;
   3. 线程上下文类加载器ContextClassLoader:Thread.currentThread().getContextClassLoader();
   4. 自定义类加载器;
二、ClassLoader和ContextClassLoader的区别
Bootstrap ClassLoader、ExtClassLoader、AppClassLoader,它们是真实存在的类,而且遵从”双亲委托“的机制。
而ContextClassLoader其实只是一个概念。
查看Thread.java源码可以发现
public class Thread implements Runnable {

/* The context ClassLoader for this thread */
   private ClassLoader contextClassLoader;

   public void setContextClassLoader(ClassLoader cl) {
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           sm.checkPermission(new RuntimePermission("setContextClassLoader"));
       }
       contextClassLoader = cl;
   }

   public ClassLoader getContextClassLoader() {
       if (contextClassLoader == null)
           return null;
       SecurityManager sm = System.getSecurityManager();
       if (sm != null) {
           ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                  Reflection.getCallerClass());
       }
       return contextClassLoader;
   }
}contextClassLoader只是一个成员变量,通过setContextClassLoader()方法设置,通过getContextClassLoader()设置。
二、线程上下文类加载器的产生原因
java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。
这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。 并且我们知道一个类由类加载器A加载,那么这个类依赖类也应该由相同的类加载器加载.那么问题来了,引导类加载器无法找到 SPI 的实现类的,因为它只加载 Java 的核心库。它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。
也就是说,双亲委派模型无法解决这个问题。
二、线程上下文类加载器的作用
ContextClassLoader的作用都是为了破坏Java类加载委托机制,使程序可以逆向使用类加载器。
比如:SPI的接口是Java核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System类加载器)来加载的。
三、线程上下文类加载器的使用
线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。
类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。
如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,Thread默认集成父线程的 Context ClassLoader(注意,是父线程不是父类)。如果你整个应用中都没有对此作任何处理,那么 所有的线程都会以System ClassLoader作为线程的上下文类加载器。
在线程中运行的代码可以通过此类加载器来加载类和资源。
在实际使用时一般都用下面的经典结构: (获取-使用-还原)
ClassLoader targetClassLoader = null;// 外部参数

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
try {
    Thread.currentThread().setContextClassLoader(targetClassLoader);
    // TODO
} catch (Exception e) {
    e.printStackTrace();
} finally {
    Thread.currentThread().setContextClassLoader(contextClassLoader);
}


  • 首先获取当前线程的线程上下文类加载器并保存到方法栈,然后将外部传递的类加载器设置为当前线程上下文类加载器
  • doSomething则可以利用新设置的类加载器做一些事情
  • 最后在设置当前线程上下文类加载器为老的类加载器
使用示例
public class ContextClassLoader {  
@Test
public void testNewThreadContxtClassLoader(){  
        Thread t2 = new Thread();  
        //输出:sun.misc.Launcher$AppClassLoader@43be2d65 新线程没有设置ContextClassLoader,将继承其父线程的上下文类加载器
        System.out.println(t2.getContextClassLoader());  
        //输出:null 因为Thread.class 是引导类加载加载的,所以其父类加载器 是为空
        System.out.println(t2.getClass().getClassLoader());  
    }  
}  四、线程上下文类加载器在SPI中 的实现
JNDI,JDBC的诉求是:
为了能让应用程序访问到这些jar包中的实现类,即用appClassLoarder去加载这些实现类。可以用getContextClassLoader取得当前线程的ClassLoader(即appClassLoarder),然后去加载这些实现类,就能让应用访问到
tomcat的诉求:
稍微跟上面有些不同,容器不希望它下面的webapps之间能互相访问到,所以不能用appClassLoarder去加载。所以tomcat新建一个sharedClassLoader(它的parent是commonClassLoader,commonClassLoader的parent是appClassLoarder,默认情况下,sharedClassLoader和commonClassLoader是同一个UrlClassLoader实例),这是catalina容器使用的ClassLoader。对于每个webapp,为其新建一个webappClassLoader,用于加载webapp下面的类,这样webapp之间就不能相互访问了。tomcat的ClassLoader 与一般类加载器的顺序是相反的(正常是先不会自己尝试加载此类,而是委托给父类的加载器去完成),首先用webappClassLoader去加载某个类,如果找不到,再交给parent。而对于java核心库,不在tomcat的ClassLoader的加载范围。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。
回复

使用道具 举报

1

主题

6

帖子

6

积分

新手上路

Rank: 1

积分
6
发表于 2023-7-15 19:19:12 | 显示全部楼层
写的非常好。tomcat 一下就点醒我了。
我还有一个困惑,关于SPI,大家都拿这个来举例,但是又感觉都没讲清楚
Class.fromName("com.mysql.cj.jdbc.Driver")
这句话,我写与不写没有什么区别。

我程序运行的时候  lib 下的包不都加载到虚拟机了吗?为什么还和TCC有关呢?
他的加载类都是 AppClassLoader
回复

使用道具 举报

3

主题

9

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-7-15 19:19:24 | 显示全部楼层
Driver 的 static 代码块调用了注册驱动的方法块,这是注册Driver的一个办法
回复

使用道具 举报

2

主题

9

帖子

11

积分

新手上路

Rank: 1

积分
11
发表于 2023-7-15 19:19:40 | 显示全部楼层
写的挺好哇
回复

使用道具 举报

4

主题

8

帖子

14

积分

新手上路

Rank: 1

积分
14
发表于 2023-7-15 19:20:36 | 显示全部楼层
写得好!
回复

使用道具 举报

4

主题

11

帖子

19

积分

新手上路

Rank: 1

积分
19
发表于 2023-7-15 19:20:42 | 显示全部楼层
从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动即不需要写Class.forName("com.mysql.cj.jdbc.Driver");,lib是按需加载。先忽略driver实现的类的代码,就先说载入实现类的class,载入class有2个方式,第一种写了classforname,app加载器直接加载进去内存。第二种不写,当要加载核心类manager时是需要driver实现类,可你boot加载不了,所以需要tcc加载driver的class,但你怎么知道加载哪个driver,接下来就是spi机制了,找到后便classforename(x.x.x.driver",flase,上下文加载器)去加载class。好了载入说完了,接下来就是实例化注册到manager  即driver实现类代码的static快,new 了个driver注册进去。打字蛮累的,有用点个赞谢谢[酷]
回复

使用道具 举报

2

主题

7

帖子

7

积分

新手上路

Rank: 1

积分
7
发表于 2023-7-15 19:21:30 | 显示全部楼层
为什么引到类加载器无法加载spi的实现类时不能交给系统类加载器去加载?双亲委派模型不就是当父类加载失败时才会交由子类加载吗
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表