深入探讨 Java 类加载器
真正理解线程上下文类加载器
对于类的加载机制的探讨主要是最近的Spring源码的第一的解读过程中总是会出现一些很有意思的东西如下:
ClassLoader ccl = Thread.currentThread().getContextClassLoader();
if (ccl == ContextLoader.class.getClassLoader()) {
currentContext = this.context;
}
else if (ccl != null) {
currentContextPerThread.put(ccl, this.context);
}
同时最近看到JDBC的接口是在核心类加载器加载的,而实现确实在App类加载器加载的,很相似的一点,都使用了一个连接的容器去连接,在JDBC中使用了CopyOnWriteArrayList而在Spring中使用了ConcurrentHashMap来连接,异曲同工的打破了,双亲委托这种机制的关系。。。。
案例如下:
以mysql为例,介绍一下驱动注册及获取connection的过程:
// 注册驱动类
Class.forName("com.mysql.jdbc.Driver").getInstance();
String url = "jdbc:mysql://localhost:3306/testdb";
// 通过java库获取数据库连接
Connection conn = java.sql.DriverManager.getConnection(url, "name", "password");
这里就需要了解的是Class.forName的用法了:
Class.forName
是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader)
和 Class.forName(String className)
。第一种形式的参数 name
表示的是类的全名;initialize
表示是否初始化类;loader
表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize
的值为 true
,loader
的值为当前类的类加载器。Class.forName
的一个很常见的用法是在加载数据库驱动的时候。
Class.forName()
加载com.mysql.jdbc.Driver
类,注意该类是java.sql.Driver
接口的实现(class Driver extends NonRegisteringDriver implements java.sql.Driver
),它们名字相同,在下面的描述中将带上package名避免混淆。
它将运行其static静态代码块:
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
实实在在的来了,沟通上下游的问题
registerDriver
方法将本类(new com.mysql.jdbc.Driver()
)注册到系统的DriverManager中,其实就是add到它的成员常量CopyOnWriteArrayList registeredDrivers
中。
好,接下来的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启动类加载器 * 但是在上一篇文章中讲到过启动类加载器无法被程序获取,所以只会得到null * 这时问题来了,DriverManager是启动类加载器加载的,可偏偏又要在这儿加载子类的Class * 子类是通过jar包的方式放入classpath中的,由AppClassLoader加载 * 因此这儿通过双亲委派方式肯定无法加载成功,因此这儿借助 * ContextClassLoader来加载mysql驱动类(简直作弊啊!) * 上一篇文章最后也讲到了Thread.currentThread().getContextClassLoader() * 默认set了AppClassLoader,也就是说把类加载器放到Thread里,那么执行方法时任何地方都可以获取到它。 */
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
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类,如果你没有修改ContextClassLoader,那么默认的AppClassLoader肯定可以加载
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");
}
这里面就有了这个关键词:获取了当前线程的ClassLoader,
Class<?> callerClass = Reflection.getCallerClass();其中线程上下文类加载器的作用已经在上面的注解中详细说明了,获取的Class也就是咱们刚刚看到的Class.forName获取的,其中用
connect()
方法获取连接,数据库厂商必须实现该方法,然而调用时DriverManager来加载外部实现类并调用
com.mysql.jdbc.Driver.connect()
来获取connection,所以这儿只能拜托Thread中保存的AppClassLoader来加载了,完全破坏了双亲委派模式。