ClassLoader的坑爹特性 - 可见性
开篇,我们先讲一下ClassLoader的坑爹特性-可见性,即:父级ClassLoader加载的类对子级ClassLoader可见,反之亦然。
接下来我们就对这一坑爹特性做一个验证,我们新建Java项目,项目中只有一个Test类,Test类有两个方法call1和call2:
- call1有两个参数 - 完整的className和一个ClassLoader。
- call2有一个参数 - 完整的className。
我们使用这两个方法进行验证。把该Java项目达成jar包,放到%JAVA_HOME%/jre/lib/ext目录下。这样当我们启动项目时test.jar就会被ExtClassLoader加载。
新建一个项目,并创建一个Girl类,该类只有一个say方法,用来打印 “帅哥,你好”。接下来我们使用Test类的call1方法,使用当前类的CassLoader对Girl类进行加载,并创建一个实例,调用say()方法。
从控制打印的信息可以看出,Test类确实是被ExtClassLoader加载的。使用当前类的ClassLoader去创建Gril实例也是没问题的。因为当前类App和Girll都是被AppClassLoader所加载的。
接下来,我们继续调用call1方法,不过这次call1的第二个参数我们不传,即Test类使用加载它的ExtClassLoader去加载并创建Girl实例:
当我们执行代码,控制台就可以看到ClassNotFoundExcption,这个完全符合ClassLoader的坑爹特性 - 父级ExtClassLoader对子AppClassLoader的加载类是不可见的。
ContextClassLoader
Thread.currentThread().getContextClassLoader(),即获取当前执行线程的ClassLoader。我们调用call2方法:
call2
call2中使用的Thread.currentThread().getContextClassLoader(),该方法获了加载App类的AppClassloader。所以这段代码可以正常运行。
由此可见ContextClassLoader解决了ClassLoader的坑爹特性 - 可见性。如果你还不清楚ClassLoader的双亲委托模式,可以看一下上一篇文章。
讲了这么多,你可能好奇这玩意到底有么斯用?接下来我们通过调试Mysql的驱动,来进一步了解ContextClassLoader的用处。
SPI
Java提供了很多服务提供者接口(Service Provide Interface,SPI),允许第三方为这些接口提供实现。常见的有JDBC、JNDI、JAXP、JBI等。
这些SPI的接口由Java核心库提供,而这些SPI的实现则是作为Java的依赖jar包被包含到ClALLPATH里。SPI接口中的代码经常需要加载第三方提供的具体实现。
有趣的是,SPI接口是Java核心库,它是由BootstrapClassLoader来加载。SPI的实现类则是由AppClassLoader来加载的。依照双亲委派模型和可见性,BootstrapClassLoader是无法获取到AppClassLoader加载的类,并不是只有人会坑爹。
JDBC
接下来,让我们来看一段熟悉又陌生的代码,我们使用JDBC连接Mysql数据库,Mysql和Oracle的驱动包我已提前放到lib中了,这里我们只使用Mysql,毕竟装一个Mysql不到10分钟,装个Oracle就......
很显然,代码是正常运行的,并打印DriverManager是由BootstrapClassLoader来加载的。
PS:如果不想看下面这段臭长的文字,可以直接放到结尾看流程。
这里我们并没有显式的指定使用Mysql的驱动,而且我也同时放了Oracle的驱动包。能够正常运行肯定是DriverManager搞事情了,我们查看一下DriverManager的源码:
查看DriverManager的源码,我们发现它有一个static代码块,执行了一个loadInitialDrivers(),通过方法名和注释(通过检查系统属性 - jdbc.properties加载最初的JDBC 驱动,然后再同通过ServiceLoader机制加载)我们可以看出就是这里搞事情了。
接下来我们就调试一个这个方法:
由于我们并没有设置系统变量“jdbc.drivers”,所以loadInitialDrivers()方法中,我们需要重点关注的就是红色框部分的代码。
接下来我们就看一下
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这行代码做了什么事情:
该load()方法,首先是获取了当前线程的ClassLoader,即main()方法所在App类的AppClassLoader,然后把java.sql.Driver的class和AppClassLoader一起传给了同名不同参的loader()方法。
该方法只有一行代码就是通过有参构造函数,创建了一个ServiceLoader实例:
通常到这里我们可能就以为完了,万万没想到,它竟然在构造函数里面调用了reload()方法。通常我们了解到的代码规范是不要在构造方法中做任何逻辑处理。
打完脸我们接着看reload()方法:
reload
reload()方法,providers.clear()清空了保存了驱动信息的LinkedHashMap,然后创建了一LazyIterator(内部类)实例,它保存了java.sql.Driver的class和AppClassLoader。
到这里算是彻底的没了。ServiceLoader.load(Driver.class)其实就干了一件事,创建了一个
ServiceLoader并保存了java.sql.Driver的class和App类的AppClassLoader到lookupIterator。
接下来应该就到了真正搞事情的环节了:
loadedDrivers.iterator()返回了一个iterator用来遍历providers,但是我们并没有发现刚的代码对providers有任何put的操作,所以providers肯定是空的。
所以搞事情的肯定是hasNext()、next()方法:
通过查看hasNext()的代码,我们没发现果然它还是对lookupIterator下了手:
hasNextService()方法,首先会获取到service(java.lang.Driver的class)的name,拼接成一个完成的fullName(文件名)然后根据这个fullName通过ClassLoader的getResources(fullName),获取所有包根目录下的
META-INF/services/java.sql.Driver文件。
我们打开msyq和oracle的驱动包看一下,里面都有这个文件:
接下来有个循环处理,但是这个条件比较特别,pending是个存放读取java.sql.Driver实现类名字的ArrayList的Iterator,默认是一个null。
第一次循环,就会走parse()方法,读取msyq驱动下
META-INF/services/java.sql.Driver文件中的内容即 - com.mysql.cj.jdbc.Driver,然后放到ArrayList中,并返回该ArrayList的Iterator。
第二次循环的时候,pending不为null,只有一个mysql的驱动,根据判断条件直接跳出了循环,读取msyql驱动类名字赋值给nextName,并返回true。
driversIterator.hasNext()返回的是true,所以进入了重点driversIterator.next()。
driversIterator.next()又调用了ServiceLoader内部类LazyIterator的next(),
接着便调用了nextService():
nextService()中的方法就比较一目了然了,根据之前传过来的AppClassLoader,去查找com.mysql.cj.jdbc.Driver的类信息,并实例化一个对象。然后放到providers中。
当然这里有很重要的一步不能忽略,那就是mysql.cj.jdbc.Driver也有一个static代码块,实例化com.mysql.cj.jdbc.Driver时,就会new一个自己的对象并注册到DriverManager的registeredDrivers中。
到这里就基本完成了mysq的坑爹注册。
第二次循环Oracle的驱动的步骤基本类似。可能有小伙伴打开oracle的驱动(oracle.jdbc.Driver)会发现没有
DriverManager.registerDriver()方法。不要慌,他有个父类oracle.jdbc.driver.OracleDriver,在oracle.jdbc.driver.OracleDriver中同样有类似的代码:
通过以上步骤,便完成了Mysql和Oracle驱动的初始化,当通过Connection connection =
DriverManager.getConnection(Const.JDBC_URL, Const.USERNAME, Const.PASSWORD)就可以获取到具体的驱动了。
可能有小伙伴会问DriverManager怎么知道我连接的是哪个驱动,答案就在jdbcUrl中的协议头,Driver.driver.connect(url, info)会校验jdbcUrl的协议头。
看到这里基本感觉这里搞得是相当的复杂,给我的感觉就是直接new一个Mysql的驱动注册给DriverManager的registeredDrivers不就好了吗?通过SPI去自动扫描
META-INF/services/java.sql.Driver触发具体JDBC驱动注册固然好,但是过程中产生了三个实例。不过竟然官方都这么做肯定有他的好处,可能是我个人水平不够体会不到。
总体的流程就是:
- 实现了java.sql.Driver的厂商,按照SPI的约定,放一个META-INF/services/java.sql.Driver在jar的根目录下
- 当运行代码时,DriverManager就会利用ServiceLoader去扫描jar下的META-INF/services/java.sql.Driver,加载并初始化这个具体的实现驱动类
- 当初始化具体的实现类,就会自动向DriverManager注册当前实现类到DriverManager的registeredDrivers中
- 当我们使用DriverManager.getConnection连接数据库时,getConnection中会循环registeredDrivers尝试校验并连接数据库