以jdbc为例搞清contextClassLoader

时间:2022-10-25 19:36:27

    在之前的一篇文章浅议tomcat与classloader中已经介绍过ClassLoader在tomcat中的应用,但是相对来说都是对双亲委派模型的比较正统的应用。

    这里再简单的介绍一下双亲委派模型,每次从底层的ClassLoader申请类加载之后都会自底向上判断这个类是否被加载过,然后再自顶向下的去加载类。双亲委派模型很简单,在ClassLoader中短短的loadClass方法就能搞定他。

以jdbc为例搞清contextClassLoader

protected synchronized Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
Class c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
if (resolve) {
resolveClass(c);
}
return c;
}

    双亲委派模型的好处是它的层次关系以及加载机制保证了安全性。比如java.land.String是由BootstrapClassLoader加载的,如果你在CustomerClassLoader中同样写了一个java.lang.String,那么可以肯定的是,你自己实现的String是肯定不会被加载的,按照这个模型,他在加载同样名字的类时,最后找到了BootstrapClassLoader中的String。显而易见的,他避免了你的恶意代码。

    但是,双亲委派模型同样也带来问题,如果在某些情况确实需要上层的ClassLoader去加载下层的类,这个模型就束手无策了。这样的情况有很多,许多文章都讲过,比如JNDI,JDBC,JAXB,JCE等,但是大多数都没讲清具体是如何运作的。

    所以接下来会以JDBC为例来分析这个问题。下面是一个获取JDBC连接的典型的例子。

try {
Class.forName("com.mysql.jdbc.Driver");
DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "aaa", "bbb");
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
    这个例子中,后面究竟干了什么呢?
    第一句Class.forName很清楚,就是去加载com.mysql.jdbc.Driver这个类,但是为什么加载了以后,DriverManager就能从他得到Connection呢?答案就在com.mysql.jdbc.Driver中。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}

public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
    现在明白了吧,是由Driver向DriverManager注册了自己,这样DriverManager才能够找到自己。值得注意的是,这里的ClassLoader是什么。显然,执行JDBC连接的测试类的加载是由AppClassLoader完成的(我在junit中写的测试),而在Driver这个类自然也是由AppClassLoader搞定,但是加载后执行的那段static方法中,java.sql.DriverManager是谁来加载的呢?如果你想到双亲委派模型,恭喜你,DriverManager就是由BootstrapClassLoader来加载的。

   接下来,调用DriverManager的getConnection方法,在getCallerClass这个方法中,会使用callerClassLoader来加载我们的驱动,而注意到最关键的地方是,callerClassLoader就是getConnection中Thread.currentThread().getContextClassLoader()。

private static Class getCallerClass(ClassLoader callerClassLoader, 
String driverClassName) {
Class callerC = null;

try {
callerC = Class.forName(driverClassName, true, callerClassLoader);
}
catch (Exception ex) {
callerC = null; // being very careful
}

return callerC;
}

private static Connection getConnection(
String url, java.util.Properties info, ClassLoader callerCL) throws SQLException {
java.util.Vector drivers = null;

synchronized(DriverManager.class) {
if(callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

if (!initialized) {
initialize();
}

synchronized (DriverManager.class){
drivers = readDrivers;
}

SQLException reason = null;
for (int i = 0; i < drivers.size(); i++) {
DriverInfo di = (DriverInfo)drivers.elementAt(i);

if ( getCallerClass(callerCL, di.driverClassName ) != di.driverClass ) {
println(" skipping: " + di);
continue;
}
try {
println(" trying " + di);
Connection result = di.driver.connect(url, info);
if (result != null) {
// Success!
println("getConnection returning " + di);
return (result);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
}

if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

    如果还不理解,那我可以问个问题,假设我们不用这个callerClassLoader会发生什么?应该明白,在DriverManager中,如果用Class.forName,而又不指定ClassLoader,那必然也会用加载DriverManager的BootstrapClassLoader来加载我们的driver,但是,driver是在类加载层次中的最下层,根据双亲委派模型,BootstrapClassLoader是不会到下层去找driver的,那必然会报ClassNotFound,这正是问题所在。而指定了Thread的ContextClassLoader便绕过了这一问题,因为当前thread的ContextClassLoader是运行junit的ClassLoader,即AppClassLoader,所以这里最终目的,便是在BootstrapClassLoader加载的类中指定AppClassLoader来加载driver。

    到这里,真相就大白了,但是细心的读者一定会发现Driver一早就被new出来并注入到DriverManager中了,为什么还要在后面去打破双亲委派机制再次加载Driver呢。其实他再次加载Driver仅仅是为了跟之前加载的driver的class做比较,确保他们是同一个加载器加载的。看到这很多人也许会奇怪,他们一直都是同一个加载器加载的啊,这可不一定了,比如我们可以派生一个CustomerClassLoader,可以指定用它来加载driver,但是执行junit的contextClassLoader仍然是AppClassLoader,这种情况下这个检查就是有用的了。