真正理解线程上下文类加载器(多案例分析)

转载自  真正理解线程上下文类加载器(多案例分析)

前置知识: java类加载器不完整分析

前言

此前我对线程上下文类加载器(ThreadContextClassLoader,下文使用TCCL表示)的理解仅仅局限于下面这段话:

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口由 Java 核心库来提供,而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)来加载的。引导类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。

而线程上下文类加载器破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器。

一直困恼我的问题就是,它是如何打破了双亲委派模型?又是如何逆向使用类加载器了?直到今天看了jdbc的驱动加载过程才茅塞顿开,其实并不复杂,只是一直没去看代码导致理解不够到位。

JDBC案例分析

我们先来看平时是如何使用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"); 

以上就是mysql注册驱动及获取connection的过程,各位可以发现经常写的Class.forName被注释掉了,但依然可以正常运行,这是为什么呢?这是因为从Java1.6开始自带的jdbc4.0版本已支持SPI服务加载机制,只要mysql的jar包在类路径中,就可以注册mysql驱动。

那到底是在哪一步自动注册了mysql driver的呢?重点就在DriverManager.getConnection()中。我们都是知道调用类的静态方法会初始化该类,进而执行其静态代码块,DriverManager的静态代码块就是:

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

初始化方法loadInitialDrivers()的代码如下:

private static void loadInitialDrivers() {String drivers;try {// 先读取系统属性drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {public String run() {return System.getProperty("jdbc.drivers");}});} catch (Exception ex) {drivers = null;}// 通过SPI加载驱动类AccessController.doPrivileged(new PrivilegedAction<Void>() {public Void run() {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;}});// 继续加载系统属性中的驱动类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);// 使用AppClassloader加载Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());} catch (Exception ex) {println("DriverManager.Initialize: load failed: " + ex);}}
}

从上面可以看出JDBC中的DriverManager的加载Driver的步骤顺序依次是: 
1. 通过SPI方式,读取 META-INF/services 下文件中的类名,使用TCCL加载; 
2. 通过System.getProperty("jdbc.drivers")获取设置,然后通过系统类加载器加载。 
下面详细分析SPI加载的那段代码。

JDBC中的SPI

先来看看什么是SP机制,引用一段博文中的介绍:

SPI机制简介 
SPI的全名为Service Provider Interface,主要是应用于厂商自定义组件或插件中。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java SPI机制的思想:我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块、xml解析模块、jdbc模块等方案。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。 
SPI具体约定 
Java SPI的具体约定为:当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

知道SPI的机制后,我们来看刚才的代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();try{while(driversIterator.hasNext()) {driversIterator.next();}
} catch(Throwable t) {
// Do nothing
}

注意driversIterator.next()最终就是调用Class.forName(DriverName, false, loader)方法,也就是最开始我们注释掉的那一句代码。好,那句因SPI而省略的代码现在解释清楚了,那我们继续看给这个方法传的loader是怎么来的。

因为这句Class.forName(DriverName, false, loader)代码所在的类在java.util.ServiceLoader类中,而ServiceLoader.class又加载在BootrapLoader中,因此传给 forName 的 loader 必然不能是BootrapLoader,复习双亲委派加载机制请看:java类加载器不完整分析 。这时候只能使用TCCL了,也就是说把自己加载不了的类加载到TCCL中(通过Thread.currentThread()获取,简直作弊啊!)。上面那篇文章末尾也讲到了TCCL默认使用当前执行的是代码所在应用的系统类加载器AppClassLoader。

再看下看ServiceLoader.load(Class)的代码,的确如此:

public static <S> ServiceLoader<S> load(Class<S> service) {ClassLoader cl = Thread.currentThread().getContextClassLoader();return ServiceLoader.load(service, cl);
}

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

到这儿差不多把SPI机制解释清楚了。直白一点说就是,我(JDK)提供了一种帮你(第三方实现者)加载服务(如数据库驱动、日志库)的便捷方式,只要你遵循约定(把类名写在/META-INF里),那当我启动时我会去扫描所有jar包里符合约定的类名,再调用forName加载,但我的ClassLoader是没法加载的,那就把它加载到当前执行线程的TCCL里,后续你想怎么操作(驱动实现类的static代码块)就是你的事了。

好,刚才说的驱动实现类就是com.mysql.jdbc.Driver.Class,它的静态代码块里头又写了什么呢?是否又用到了TCCL呢?我们继续看下一个例子。

使用TCCL校验实例的归属

com.mysql.jdbc.Driver加载后运行的静态代码块:

static {try {// Driver已经加载到TCCL中了,此时可以直接实例化java.sql.DriverManager.registerDriver(new com.mysql.jdbc.Driver());} catch (SQLException E) {throw new RuntimeException("Can't register driver!");}
}

registerDriver方法将driver实例注册到系统的java.sql.DriverManager类中,其实就是add到它的一个名为registeredDrivers的静态成员CopyOnWriteArrayList中 。

到此驱动注册基本完成,接下来我们回到最开始的那段样例代码:java.sql.DriverManager.getConnection()。它最终调用了以下方法:

private static Connection getConnection(String url, java.util.Properties info, Class<?> caller) throws SQLException {/* 传入的caller由Reflection.getCallerClass()得到,该方法* 可获取到调用本方法的Class类,这儿调用者是java.sql.DriverManager(位于/lib/rt.jar中),* 也就是说caller.getClassLoader()本应得到Bootstrap启动类加载器* 但是在上篇文章[java类加载器不完整分析]中讲到过启动类加载器无法被程序获取,所以只会得到null*/ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;synchronized(DriverManager.class) {// 此处再次获取线程上下文类加载器,用于后续校验if (callerCL == null) {callerCL = Thread.currentThread().getContextClassLoader();}}if(url == null) {throw new SQLException("The url cannot be null", "08001");}SQLException reason = null;// 遍历注册到registeredDrivers里的Driver类for(DriverInfo aDriver : registeredDrivers) {// 使用线程上下文类加载器检查Driver类有效性,重点在isDriverAllowed中,方法内容在后面if(isDriverAllowed(aDriver.driver, callerCL)) {try {println("    trying " + aDriver.driver.getClass().getName());// 调用com.mysql.jdbc.Driver.connect方法获取连接Connection con = aDriver.driver.connect(url, info);if (con != null) {// Success!return (con);}} catch (SQLException ex) {if (reason == null) {reason = ex;}}} else {println("    skipping: " + aDriver.getClass().getName());}}throw new SQLException("No suitable driver found for "+ url, "08001");}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {boolean result = false;if(driver != null) {Class<?> aClass = null;try {// 传入的classLoader为调用getConnetction的线程上下文类加载器,从中寻找driver的class对象aClass =  Class.forName(driver.getClass().getName(), true, classLoader);} catch (Exception ex) {result = false;}// 注意,只有同一个类加载器中的Class使用==比较时才会相等,此处就是校验用户注册Driver时该Driver所属的类加载器与调用时的是否同一个// driver.getClass()拿到就是当初执行Class.forName("com.mysql.jdbc.Driver")时的应用AppClassLoaderresult = ( aClass == driver.getClass() ) ? true : false;}return result;
}

可以看到这儿TCCL的作用主要用于校验存放的driver是否属于调用线程的Classloader。例如在下文中的tomcat里,多个webapp都有自己的Classloader,如果它们都自带 mysql-connect.jar包,那底层Classloader的DriverManager里将注册多个不同类加载器的Driver实例,想要区分只能靠TCCL了。

Tomcat与spring的类加载器案例

接下来将介绍《深入理解java虚拟机》一书中的案例,并解答它所提出的问题。(部分类容来自于书中原文)

Tomcat中的类加载器

在Tomcat目录结构中,有三组目录(“/common/*”,“/server/*”和“shared/*”)可以存放公用Java类库,此外还有第四组Web应用程序自身的目录“/WEB-INF/*”,把java类库放置在这些目录中的含义分别是:

  • 放置在common目录中:类库可被Tomcat和所有的Web应用程序共同使用。
  • 放置在server目录中:类库可被Tomcat使用,但对所有的Web应用程序都不可见。
  • 放置在shared目录中:类库可被所有的Web应用程序共同使用,但对Tomcat自己不可见。
  • 放置在/WebApp/WEB-INF目录中:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。

为了支持这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现,如下图所示

灰色背景的3个类加载器是JDK默认提供的类加载器,这3个加载器的作用前面已经介绍过了。而 CommonClassLoader、CatalinaClassLoader、SharedClassLoader 和 WebAppClassLoader 则是 Tomcat 自己定义的类加载器,它们分别加载 /common/*、/server/*、/shared/* 和 /WebApp/WEB-INF/* 中的 Java 类库。其中 WebApp 类加载器和 Jsp 类加载器通常会存在多个实例,每一个 Web 应用程序对应一个 WebApp 类加载器,每一个 JSP 文件对应一个 Jsp 类加载器。

从图中的委派关系中可以看出,CommonClassLoader 能加载的类都可以被 CatalinaClassLoader 和 SharedClassLoader 使用,而 CatalinaClassLoader 和 SharedClassLoader 自己能加载的类则与对方相互隔离。WebAppClassLoader 可以使用 SharedClassLoader 加载到的类,但各个 WebAppClassLoader 实例之间相互隔离。而 JasperLoader 的加载范围仅仅是这个 JSP 文件所编译出来的那一个 Class,它出现的目的就是为了被丢弃:当服务器检测到 JSP 文件被修改时,会替换掉目前的 JasperLoader 的实例,并通过再建立一个新的 Jsp 类加载器来实现 JSP 文件的 HotSwap 功能。

Spring加载问题

Tomcat 加载器的实现清晰易懂,并且采用了官方推荐的“正统”的使用类加载器的方式。这时作者提一个问题:如果有 10 个 Web 应用程序都用到了spring的话,可以把Spring的jar包放到 common 或 shared 目录下让这些程序共享。Spring 的作用是管理每个web应用程序的bean,getBean时自然要能访问到应用程序的类,而用户的程序显然是放在 /WebApp/WEB-INF 目录中的(由 WebAppClassLoader 加载),那么在 CommonClassLoader 或 SharedClassLoader 中的 Spring 容器如何去加载并不在其加载范围的用户程序(/WebApp/WEB-INF/)中的Class呢?

解答

答案呼之欲出:spring根本不会去管自己被放在哪里,它统统使用TCCL来加载类,而TCCL默认设置为了WebAppClassLoader,也就是说哪个WebApp应用调用了spring,spring就去取该应用自己的WebAppClassLoader来加载bean,简直完美~

源码分析

有兴趣的可以接着看看具体实现。在web.xml中定义的listener为org.springframework.web.context.ContextLoaderListener,它最终调用了org.springframework.web.context.ContextLoader类来装载bean,具体方法如下(删去了部分不相关内容):

public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {try {// 创建WebApplicationContextif (this.context == null) {this.context = createWebApplicationContext(servletContext);}// 将其保存到该webapp的servletContext中     servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);// 获取线程上下文类加载器,默认为WebAppClassLoaderClassLoader ccl = Thread.currentThread().getContextClassLoader();// 如果spring的jar包放在每个webapp自己的目录中// 此时线程上下文类加载器会与本类的类加载器(加载spring的)相同,都是WebAppClassLoaderif (ccl == ContextLoader.class.getClassLoader()) {currentContext = this.context;}else if (ccl != null) {// 如果不同,也就是上面说的那个问题的情况,那么用一个map把刚才创建的WebApplicationContext及对应的WebAppClassLoader存下来// 一个webapp对应一个记录,后续调用时直接根据WebAppClassLoader来取出currentContextPerThread.put(ccl, this.context);}return this.context;}catch (RuntimeException ex) {logger.error("Context initialization failed", ex);throw ex;}catch (Error err) {logger.error("Context initialization failed", err);throw err;}
}

具体说明都在注释中,spring考虑到了自己可能被放到其他位置,所以直接用TCCL来解决所有可能面临的情况。

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/326901.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker4Dev #6 使用 Windows Container 运行.net应用

关于d4d系列&#xff1a;之前这个系列叫做docker4dotnet&#xff0c;因为当时主要是为了能够探索在docker上运行.net应用&#xff1b;现在我觉得应该叫做docker4developer&#xff0c;因为我希望能够帮助更多的开发人员使用docker技术。今天这篇就算是新的Docker4Dev系列的开始…

Windows 容器

什么是容器 它们是隔离、资源控制且可移植的操作环境。 基本上&#xff0c;容器是一个隔离的位置&#xff0c;应用程序可在其中运行&#xff0c;而不会影响系统的其他部分&#xff0c;并且系统也不会影响该应用程序。 容器是虚拟化的下一个演化。 如果你在容器内&#xff0c;看…

选择排序+推导过程

图解 代码实现 package com.atguigu.sort;import java.util.Arrays; import java.util.List;/*** 创建人 wdl* 创建时间 2021/3/21* 描述*/ public class SelectSort {public static void main(String[] args) {int []arr{101,34,119,1};System.out.println("排序前"…

Echart折线图 柱状图

echat_百度搜索 Examples - Apache ECharts Examples - Apache ECharts Examples - Apache ECharts 修改左侧的数据 点击右侧下载可以得到html页面 Examples - Apache ECharts Examples - Apache ECharts

软件定义数据中心—Windows Server SDDC技术与实践

《软件定义数据中心—Windows Server SDDC技术与实践》是国内第一本讲解微软Windows Server 软件定义数据中心的中文图书&#xff0c;书中系统、全面地介绍了微软Windows Server 软件定义数据中心各个模块&#xff08;SDS/SDN/SDC/容器&#xff09;的概念、技术和架构&#xff…

插入排序+思路分析

图解 代码实现 package com.atguigu.sort;import java.util.Arrays;/*** 创建人 wdl* 创建时间 2021/3/22* 描述*/ public class InsertSort {public static void main(String[] args) {int[] arr{101,34,119,1};insertSort(arr);}//插入排序public static void insertSort(in…

.NET 十五岁,谈谈我眼中的.NET

2002年2月13日&#xff0c;第一个版本随着visual studio.net的发布&#xff0c;今天已经走过15年, .net团队写了一篇文章&#xff0c;里面有一个视频&#xff0c;Anders Hejlsberg已是白发苍苍的老人&#xff0c;我也从刚出校门的码农长成软件开发工程师&#xff0c;我爱编程。…

理解并从头搭建redis集群

部分开发人员工作当中只是在应用中使用redis&#xff0c;比如用来做数据结果的缓存。而且现在有很多不错的redis客户端工具(redisson)&#xff0c;基本上可以不用关注redis命令就可以完成相当部分的功能。所以可能会对如下这些问题关注点不够&#xff1a; 如何容灾&#xff1f;…

希尔排序+过程分析

图解 代码实现 package com.atguigu.sort;import java.util.Arrays;/*** 创建人 wdl* 创建时间 2021/3/22* 描述*/ public class ShellSort {public static void main(String[] args) {int[] arr {8, 9, 1, 7, 2, 3, 5, 4, 6, 0};shellSort(arr);}//使用逐步推导的方式来编写…

Java 程序员必须掌握的 5 个注解

转载自 Java 程序员必须掌握的 5 个注解 自 JDK5 推出以来&#xff0c;注解已成为Java生态系统不可缺少的一部分。虽然开发者为Java框架&#xff08;例如Spring的Autowired&#xff09;开发了无数的自定义注解&#xff0c;但编译器认可的一些注解非常重要。 在本文中&#xff…

Docker4Dev#7 使用 Windows Container运行ASP.NET MVC 2 + SQLExpress 应用

上一篇Windows Container文章中给大家介绍了如何使用Windows Container运行一个传统的.net 4.5 web应用程序&#xff0c;当时我们使用了默认的Visual Studio模版创建了一个简单的项目&#xff0c;而且没有链接数据库。我相信使用.net进行应用开发的程序员们一定在想&#xff0c…

Mybatis+MySQL动态分页查询数据经典案例

最近在用Mybatis做项目的时候遇到了不少问题&#xff0c;今天我就在这和大家分享一下&#xff0c;稀稀拉拉的研究了两天&#xff0c;终于搞好了&#xff01;开发人员&#xff1a;1111开发软件&#xff1a;Myeclipse用到的框架技术&#xff1a;Mybatis数据库&#xff1a;MySql主…

那些年的骗子

中午一觉睡醒之后&#xff0c;忽然发现多年不联系的初中同学给我发了个消息&#xff0c;觉得事情没那么简单&#xff1a;正好我的公众号需要大量的用户&#xff0c;我就顺水推舟了&#xff01;一看到QQ的安全提示&#xff0c;我就感觉事情确实不妙&#xff01;初步推断对方是个…

干货 | 彻底弄懂 HTTP 缓存机制及原理

转载自 干货 | 彻底弄懂 HTTP 缓存机制及原理 前言 Http 缓存机制作为 web 性能优化的重要手段&#xff0c;对于从事 Web 开发的同学们来说&#xff0c;应该是知识体系库中的一个基础环节&#xff0c;同时对于有志成为前端架构师的同学来说是必备的知识技能。 但是对于很多…

Visual Basic的未来之路

上周&#xff0c;微软宣布了他们改变Visual Basic语言未来发展计划的想法。这次公布给Visual Basic开发人员留下了很多不确定性&#xff0c;但Visual Basic语言的设计者Anthony D.Green说明了这个新策略的一些细节。 Green首先列出了当时使用VB进行开发的四个基础指导原则&…

快速排序+思路分析

图解 代码实现 package com.atguigu.sort;import com.sun.org.apache.xpath.internal.WhitespaceStrippingElementMatcher;import java.util.Arrays;/*** 创建人 wdl* 创建时间 2021/3/22* 描述*/ public class QuickSort {public static void main(String[] args) {//[-9,78,…

jquery sleep函数

function sleep(n) { //n表示的毫秒数 var start new Date().getTime(); while (true) if (new Date().getTime() - start > n) break; } console.log(new Date()); this.sleep(3000); console.log(new Date());

2017 年编程语言排行榜:Python 排第一

站长之家(ChinaZ.com) 7 月 24 日消息&#xff0c;近日根据 IEEE Spectrum 发布的研究报告显示&#xff0c;在 2016 年排名第三的 Python 在今年已经成为世界上最受欢迎的语言&#xff0c;C 和 Java 分别位居第二和第三位。IEEE Spectrum 的排行依据数据记者 Nick Diakopoulos …

Spring 获取 request 的几种方法及其线程安全性分析

转载自 Spring 获取 request 的几种方法及其线程安全性分析 本文将介绍在Spring MVC开发的Web系统中&#xff0c;获取request对象的几种方法&#xff0c;并讨论其线程安全性。 一、概述 在使用Spring MVC开发Web系统时&#xff0c;经常需要在处理请求时使用request对象&…

2的负x次幂图像_数学| NO.2,3 函数 T15

点击蓝字&#xff0c;关注我们函数题目2020.11.30#2,3函数 T15函数发展&#xff1a;函数就是在某变化过程中有两个变量X和Y&#xff0c;变量Y随着变量X一起变化&#xff0c;而且依赖于X。如果变量X取某个特定的值&#xff0c;Y依确定的关系取相应的值&#xff0c;那么称Y是X的函…