前面的分享中提到的最多的概念就是关于类加载器的概念,但是当我们查看Thread源码的时候会发现如下的两个方法,这两个方法就是获取或者设置线程的上下文类加载器的方法,那么为什么要设置这两个方法呢?这个就是这次分享所要说的事情。
线程上下文类加载器使用
在Thread类中有两个方法,如下
获取到当前线程的上下文类加载器
public ClassLoader getContextClassLoader() {
//如果获取上下文类加载器为空
if (contextClassLoader == null)
return null;
//获取系统校验
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(contextClassLoader,
Reflection.getCallerClass());
}
return contextClassLoader;
}
设置当前线程的上下文类加载器
public void setContextClassLoader(ClassLoader cl) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("setContextClassLoader"));
}
contextClassLoader = cl;
}
如果当前线程没有设置上下文类加载器,那么它就会和父线程保持同样的类加载器,也就是说Main线程使用什么样的类加载器其子类线程就使用什么样的类加载器。如下
public class ThreadTest {
public static void main(String[] args) {
System.out.println(currentThread().getContextClassLoader());
}
}
那么为什么会有线程上下文类加载器的出现呢?这个就是因为我们类加载的双亲委托机制的缺陷,因为在类加载的过程中会一层一层的委托给父类加载器去加载,而我们最终的加载都会到BootStrapClassLoader类加载中进行加载,在JDK核心类库中提供了很多SPI(Service Provider Interface),其中比较常用的就是关于JDBC的SPI接口。JDK也只是规定的这个接口的之间的实现逻辑关系,并没有提供具体的怎么样的实现。这些都是我们通过第三方的厂商来提供的,例如使用MySQL的时候我们要引入MySQL的驱动,使用Oracle的时候要使用Oracle的驱动等等。如图所示,Java使用JDBC这个SPI完全的实现了应用和第三方数据库驱动的实现的接入。在使用的时候只需要更换对应的jar包和数据库驱动,其他的一概不变。
这样做的好处是JDBC提供了对于数据库操作的高度封装,应用程序只需要实现对应的接口即可,而对于我们持久层的框架来说就是对这些接口的再次的封装,使得我们开发更加的简便。从上图也可以看出我们在实际使用的时候各大数据库提供者都实现了具体的底层驱动,使用者并不需要关心这些。
就拿MySQL驱动举例子来说,在使用的时候我们是通过Class.forName()这个方法来将数据库的驱动引入到其中。但是是谁去加载其中的Class文件呢?答案当然是BootstrapClassLoader,第三方的驱动包是不会提供类加载器进行加载的,交给JVM使用双亲委托机制之后最后的结果就是使用根类加载器进行加载。下面我们就来深入的分析一下关于MySQL驱动的的初始化以及源码加载过程。
MySQL数据库驱动的初始化操作
示例
public class JDBCTest {
private static Connection connection;
private static PreparedStatement preparedStatement;
private static ResultSet resultSet;
public static void main(String[] args) throws SQLException {
try {
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection("null","null","null");
preparedStatement.execute("SELECT * FROM student");
resultSet = preparedStatement.executeQuery();
while (resultSet.next()){
System.out.println(resultSet.getInt(1)+resultSet.getInt(2));
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
在使用JDBC的时候第一件事情就是加载对应的驱动。这里我们就来看一下关于这驱动里面都有什么样的东西。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
会看到实际上,在Driver的静态代码块中主要的内容是将Driver注册给了DriverManager。这个作用与Class.forName效果是一样的。我们会看到在示例代码中第二步是通过DriverManager获取到对应的连接。下面就是比较关键的部分了
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
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");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// if we got here nobody could connect.
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");
}
- 第一步获取到对应的类加载器,而从这里看到,我们所使用的三个参数的方法,是对这个方法的一个封装。也就是说在使用的时候进行了重载。而这个重载类似于代理模式,我们使用的只不过是这个方法的代理方法,这个方法的实际实现并没有被破坏。
- 如果没有获取到对应的类加载器的话就尝试获取线程上下文类加载器。
- 通过递归DriverManager中已经注册的驱动类,验证该数据库驱动是否可以被指定的线程上下文类加载进行加载,如果通过则返回Connection,也就是说这个Connection就是需要的数据库提供的实例
通过上面的分析,我们也知道了在JDK中提供的SPI接口,就是对于数据库驱动与JDK核心实现进行捆绑。但是由于JVM类加载器机制的显示,启动类不可能直接加载到对应的第三方实现的操作。所以JDK提供了线程上下文类加载器。通过这个机制,启动类将类加载的工作交给了子类SPI实现。
至此实际上变成了父类委托子类加载的过程,也打破了双亲委托机制,而几乎以后所有的对于设计SPI的加载的动作都是通过这种方式实现的。其中个人研究比较多的就是Dubbo中的SPI机制。当然这个SPI机制在实现的时候还是存在一些BUG的。例如很多的日志框架的实现。所以在实际操作中对于日志框架的选择还是要值得注意的。
总结
通过分析MySQL驱动的实现源码。了解了关于上下文类加载器的作用。这种机制打破了类加载的双亲委托机制。但是提供了一种解决第三方依赖的解决方案。但是尽管是这样,这个机制还是不完美的,对于Java安全问题还是有待商榷的,例如对于某编程软件的破解。