打破双亲委派的两种场景
前言
Java
中的双亲委派机制可以保证Java
的运行安全,保证Java
中的核心类可以被正确安全加载。那有没有打破双亲委派机制的Java
应用呢?其实是有的,比如运行Java Web
应用的Tomcat
容器以及Java
专门用来操作数据库的API
—JDBC
,它们就都打破了双亲委派机制了。为什么它们如此特殊呢?
Tomcat打破双亲委派
通常使用Java
开发Web
应用时,最后我们都会将源码打包成一个war
包或者jar
包,然后扔进Tomcat
容器里去运行。通常情况下,一台服务器上只会部署一个Tomcat
容器,现在因为公司资源有限,我想在Tomcat
里部署多个应用可不可以?肯定是可以的,只要server.port
设置不同,就可以在Tomcat
里部署多个应用。那现在问题来了,不同应用里可能使用了同一个jar
包的不同版本,就比如常见的lombok
,就有很多个不同的版本。如果Tomcat
还是使用Java
默认的双亲委派机制(注:Tomcat
也是Java
写的),那就只会加载一个lombok
?那Tomcat
到底加载哪个?还有就是如mysql-connector-java
这种比较特殊的,如果mysql
是5.7版本的,只能使用mysql-connector-java
的特定版本来加载。此时如果Tomcat
还是遵从双亲委派就无法支持这种场景。
Tomcat的类加载机制
上图就是Tomcat
中的类加载机制。可以看到,最顶层的三个类加载器还是Java
中默认的类加载器。Common ClassLoader
、Catalina ClassLoader
、Shared ClassLoader
、Webapp ClassLoader
和Jasper Loader
则是Tomcat
内部自定义的类加载器。
-
Common ClassLoader
加载的类都可以被Catalina ClassLoader
和Share ClassLoader
使用,从而实现公有类库的公用 -
Catalina ClassLoader
和Share ClassLoader
各自加载的类则与对方相互隔离,实现了Tomcat
自身依赖与Web
应用依赖的隔离 -
WebApp ClassLoader
加载每个应用/WebApp/WEB-INF/*
目录下的资源,WebApp ClassLoader
可以使用Share ClassLoader
加载的类,但各个
WebApp ClassLoader
之间相互隔离 -
Jasper Loader
主要是用来加载JSP
文件的。JSP
其实也是Java
文件,其编译后也会生成一个.class
文件 -
Jasper Loader
的加载范围仅仅是这个JSP
文件所编译出来的那个.class
文件,一对一的设计是为了随时丢弃它,以实现热加载功能 -
Tomcat
会在后台启动一个线程来监听JSP
文件变化,如果这个JSP
文件变化了则找到这个JSP
对应的Servlet
的类加载器对象引用,重新生成新的Jasper Loader
类加载器对象赋值,然后加载新的JSP
对应的Servlet
类,原先的那个类加载器对象会在下次GC被回收综上,
Tomcat
打破了双亲委派机制,让每个WebApp ClassLoader
加载自己目录下的class
文件,没有向上传递给父加载器。Tomcat
通过这样一套类加载机制,实现了各个Web
应用之间,相同依赖版本的统一加载,不同依赖版本的分别加载以及随时热加载JSP
文件的功能。
JDBC打破双亲委派
Java
开发难免少不了与数据库打交道。而JDBC
是Java
中与数据库连接的API
。JDBC
的Driver
是定义在rt.jar
包中的,但是它的实现通常是由不同的数据库厂商来完成的,如mysql-connetor-java
。同时rt.jar
包中的DriverManager
类会加载每个Driver
接口的实现类并管理它们。那么问题来了,根据类加载机制,某个类需要引用其它类的时候,虚拟机将会用这个类的classloader
去加载被引用的类。既然DriverManager
是由BootStrap ClassLoader
加载,那么各个Driver
的实现类理应也由BootStrap ClassLoader
来加载。但很显然,Boostrap Classloader
显然是无法加载到mysql-connetor-java
的,二者明显不在一个目录里,直接报ClassNotFound。因此只能在DriverManager
里强行指定下层的classloader
来加载Driver
实现类,而这就会打破双亲委派模型。
JDBC实现方式
在JDBC 4.0
之后,借助于Java
中SPI机制,可以实现自动加载。通过查看DriverManager
类的代码可以看到,当我们使用DriverManager
的时候就会触发static
代码块,进而会加载META-INF/services/java.sql.Driver
指定的类。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
AccessController.doPrivileged(new PrivilegedAction<Void>() { // 1. AccessController,Java安全模型
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 2. 核心,ServiceLoader就是JDK提供的SPI的实现方式
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next(); // 3. 遍历的过程会触发每个Driver实现类的加载
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
}
ServiceLoader#load
方法中,通过获取Thread.currentThread()
的context class loader
来根据配置加载对应的类。
public static <S> ServiceLoader<S> load(Class<S> service) {
// Thread.currentThread().getContextClassLoader() 默认是系统类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Thread.currentThread().getContextClassLoader()默认返回应用类加载器,这个可以从sun.misc.Launch
类中看到。
public class Launcher {
public Launcher() {
...
try {
//应用类加载器
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
} catch (IOException var9) {
throw new InternalError("Could not create application class loader", var9);
}
Thread.currentThread().setContextClassLoader(this.loader);
...
}
}
总结
分析了两种打破Java
双亲委派的情况,希望大家看完总结消化一下。