Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存中生成class对象,而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式.
何为沙箱安全机制?即如果我们自己建立和源代码相同的包,例如java/lang/String.Class,在我们去使用类加载器去加载此类时,为了防止你自定义的类对源码的破坏,所以他默认不是使用你的String类的本身的系统加载器去加载它,而是选择率先使用引导类加载器去加载,而引导类在加载的过程中会先去加载JDK自带的文件(rt.jar包中的java/lang/String.class),而不是你自己定义的String.class,报错信息会提示没有main方法 ,就是因为加载的是rt.jar包下的String类,这样就可以做到保证对java核心源代码的保护,这即是沙箱保护机制.
那么双亲委派机制存在哪些缺点,又有哪些打破双亲委派机制的例子呢?
通过上面双亲委派机制的特点,我们知道了以下结论:
由于BootStrapClassLoader是顶级类加载器,并且它不能使用AppClassLoader加载器加载的类,因为BootstrapClassloader无法委派AppClassLoader来加载类。故当我们的上层类加载器需要下层类加载器帮忙加载类,就会出现问题,这便是双亲委派机制的缺点,那么哪些场景发生了这些事情,而Java又是如何打破这种双亲委派机制的呢?
我们来举一个经典的打破双亲委派机制的例子:Java SPI机制
SPI(Service Provider Interface),主要是应用于厂商自定义组件或插件中。
java SPI提供一种服务发现的机制,即为某个接口寻找服务实现的机制,我们在需要在jar包的META-INF/services/目录里面创建一个一服务接口命令的文件,而该文件就是实现这个接口的具体实现类,当有外部程序需要装配我们的这个模块接口的时候,就可以通过META-INF/services/目录的配置文件找到对应的实现类的类名,并将其实例化,完成该接口模块的注入。而在JDK中,也提供了一个服务实现查找的一个工具类:java.util.ServiceLoader。
我们以数据库连接jar包为例:mysql-connector-java-5.1.37.jar,它遵循SPI的规范
我们的java.sql.Driver接口定义在java.sql包下,而该包的位置位于jdk\jre\lib\rt.jar,其中java.sql包中还提供了其它相应的类和接口比如管理驱动的类:DriverManager类等等,这样我们就很清楚地知道了java.sql是由BootStrapClassLoader加载器加载的
而我们的接口的实现类com.mysql.jdbc.Driver是第三方实现的类库,是由AppClassLoader加载器加载的
那么问题来了:我们使用DriverManager获取Connection的时候,必然会加载到com.mysql.jdbc.Driver类,此时即是BootstrapClassloader加载的类使用了由AppClassLoader加载的类,很明显和双亲委托机制的原理相悖!!!
JVM采取了一种作弊的方式打破了双亲委派机制,从而完成了上层类加载器使用下层类加载器进行加载的类:
在BootstrapClassLoader或ExtClassLoader加载的类A中,如果使用到AppClassLoader类加载器加载的类B,由于双亲委托机制不能向下委托,那可以在类A中通过线程上下文类加载器获得AppClassLoader,从而去加载类B
我们通过源码来看一下:
首先我们创建一个web项目,然后导入mysql的驱动包,我们编写一下测试代码:
public class TestClassLoader {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/mybatis";
String username = "root";
String password = "root";
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
//执行驱动"com.mysql.jdbc.Driver"
Class.forName("com.mysql.jdbc.Driver");
//获取连接
connection = DriverManager.getConnection(url,username,password);
}
}
此时我们知道这段代码肯定是能正常执行了,第一步加载我们的mysql的驱动,然后利用DriverManager获取连接
那么我们将第一步的执行驱动代码注释掉,你猜猜还会执行成功吗??
根据上面我们说的,答案是肯定的!!!我们进入源码探究一下
在DriverManager类的内部存在一个静态代码块,其中有一个**loadInitialDrivers()**方法:
我们点进去,里面有一行代码:
//JDK中提供的ServiceLoader来实现服务实现查找功能
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
我们点进去这个方法看看:
public static ServiceLoader load(Class service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这段代码便是打破双亲委派机制,实现com.mysql.jdbc.Driver驱动类加载的核心,即我们获取线程上下文加载器Thread.currentThread().getContextClassLoader(),然后利用线程上下文类加载器进行mysql驱动的类加载,以达到打破双亲委派机制的作用
PS:此时如果我们改变线程上下文类加载器,就会使得此时程序出现错误,大家也就明白了其中的流程和原理了
public class TestClassLoader {
public static void main(String[] args) throws SQLException, ClassNotFoundException {
String driverClassName = "com.mysql.jdbc.Driver";
String url = "jdbc:mysql://localhost:3306/mybatis";
String username = "root";
String password = "root";
Connection connection = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
//改变当前线程上下文类加载器为当前类的扩展类加载器
Thread.currentThread().setContextClassLoader(TestClassLoader.class.getClassLoader().getParent());
//执行驱动"com.mysql.jdbc.Driver"
//Class.forName("com.mysql.jdbc.Driver");
//获取连接
connection = DriverManager.getConnection(url,username,password);
}
}
执行结果:
Exception in thread "main" java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/mybatis
at java.sql.DriverManager.getConnection(DriverManager.java:689)
at java.sql.DriverManager.getConnection(DriverManager.java:247)
at it.feng.test.TestClassLoader.main(TestClassLoader.java:25)