深度思考:老生常谈的双亲委派机制,JDBCTomcat是怎么反其道而行之的?
Posted SunAlwaysOnline
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度思考:老生常谈的双亲委派机制,JDBCTomcat是怎么反其道而行之的?相关的知识,希望对你有一定的参考价值。
要说双亲委派机制,还得从类加载器的类型谈起
一、类加载器的类型
类加载器有以下种类:
- 启动类加载器(Bootstrap ClassLoader)
- 扩展类加载器(Extension ClassLoader)
- 应用类加载器(Application ClassLoader)
启动类加载器
内嵌在JVM内核中的加载器,由C++语言编写(因此也不会继承ClassLoader),是类加载器层次中最顶层的加载器。用于加载java的核心类库,即加载jre/lib/rt.jar里所有的class。由于启动类加载器涉及到虚拟机本地实现细节,我们无法获取启动类加载器的引用。
扩展类加载器
它负责加载JRE的扩展目录,jre/lib/ext或者由java.ext.dirs系统属性指定的目录中jar包的类。父类加载器为启动类加载器,但使用扩展类加载器调用getParent依然为null。
应用类加载器
又称系统类加载器,可用通过 java.lang.ClassLoader.getSystemClassLoader()方法获得此类加载器的实例,系统类加载器也因此得名。应用类加载器主要加载classpath下的class,即用户自己编写的应用编译得来的class,调用getParent返回扩展类加载器。
扩展类加载器与应用类加载器继承结构如图所示:
可以看到除了启动类加载器,其余的两个类加载器都继承于ClassLoader,我们自定义的类加载器,也需要继承ClassLoader。
值得注意的是,启动类、扩展类与应用类加载器之间的父子关系,并不是通过继承来实现的,而是通过组合,即使用parent变量来保存“父加载器”的引用。
二、双亲委派机制
当一个类加载器收到了一个类加载请求时,它自己不会先去尝试加载这个类,而是把这个请求转交给父类加载器,每一个层的类加载器都是如此,因此所有的类加载请求都应该传递到最顶层的启动类加载器中。只有当父类加载器在自己的加载范围内没有搜寻到该类时,并向子类反馈自己无法加载后,子类加载器才会尝试自己去加载。
加载标准类库与用户代码,会有不同的方式:
ClassLoader内的loadClass方法,就很好的解释了双亲委派的加载过程:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//检查该class是否已经被当前类加载器加载过
Class<?> c = findLoadedClass(name);
if (c == null) {
//此时该class还没有被加载
try {
if (parent != null) {
//如果父加载器不为null,则委托给父类加载
c = parent.loadClass(name, false);
} else {
//如果父加载器为null,说明当前类加载器已经是启动类加载器,直接时候用启动类加载器去加载该class
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
}
if (c == null) {
//此时父类加载器都无法加载该class,则使用当前类加载器进行加载
long t1 = System.nanoTime();
c = findClass(name);
...
}
}
//是否需要连接该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
三、双亲委派存在的意义
为什么要使用双亲委派机制呢?
假设用户自己定义了java.lang.Object类,由于双亲委派机制的存在,最终会委托到启动类加载器去加载,即返回rt.jar中的Object类,并不会加载用户编写的Object类。
大家上班摸鱼刷的LeetCode,本质上自定义了一个类加载器,重写了findClass方法,会从网络中加载字节码,生成Class对象,最终通过loadClass定义的双亲委派机制进行加载。如果这个时候,我定义了一个恶意java.lang.Object类,在没有双亲委派机制的情况下,可能会对jvm产生安全风险。
双亲委派机制存在的意义,就是为了防止findClass与defineclass生成的Class对象覆盖掉标准类库中的基础类,避免产生安全风险。
四、如何自定义类加载器
我们整理ClassLoader里面的流程
- loadclass:双亲委派机制,子加载器委托父加载器加载,父加载器都加载失败时,子加载器通过findclass自行加载
- findclass:当前类加载器根据路径以及class文件名称加载字节码,从class文件中读取字节数组,然后使用defineClass
- defineclass:根据字节数组,返回Class对象
我们在ClassLoader里面找到findClass方法,发现该方法直接抛出异常,应该是留给子类实现的。
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
到这里,我们应该明白,loadClass方法使用了模版方法模式,主线逻辑是双亲委派,但如何将class文件转化为Class对象的步骤,已经交由子类去实现。对模版方法模式不熟悉的同学,可以先参考我的另外一篇文章模版方法模式
其实源码中,已经有一个自定义类加载的样例代码,在注释中:
class NetworkClassLoader extends ClassLoader {
String host;
int port;
public Class findClass(String name) {
byte[] b = loadClassData(name);
return defineClass(name, b, 0, b.length);
}
private byte[] loadClassData(String name) {
// load the class data from the connection
}
}
看得出来,如果我们需要自定义类加载器,只需要继承ClassLoader,并且重写findClass方法即可。
现在有一个简单的样例,class文件依然在文件目录中:
package com.yang.testClassLoader;
import sun.misc.Launcher;
import java.io.*;
public class MyClassLoader extends ClassLoader {
/**
* 类加载路径,不包含文件名
*/
private String path;
public MyClassLoader(String path) {
super();
this.path = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] bytes = getBytesFromClass(name);
assert bytes != null;
//读取字节数组,转化为Class对象
return defineClass(name, bytes, 0, bytes.length);
}
//读取class文件,转化为字节数组
private byte[] getBytesFromClass(String name) {
String absolutePath = path + "/" + name + ".class";
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try {
fis = new FileInputStream(new File(absolutePath));
bos = new ByteArrayOutputStream();
byte[] temp = new byte[1024];
int len;
while ((len = fis.read(temp)) != -1) {
bos.write(temp, 0, len);
}
return bos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bos) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
MyClassLoader classLoader = new MyClassLoader("C://develop");
Class test = classLoader.loadClass("Student");
test.newInstance();
}
}
Student类:
public class Student {
public Student() {
System.out.println("student classloader is" + this.getClass().getClassLoader().toString());
}
}
注意,这个Student类千万不要加包名,idea报错不管他即可,然后使用javac Student.java编译该类,将生成的class文件复制到c://develop下即可。
运行MyClassLoader的main方法后,可以看到输出:
看得出来,Student.class确实是被我们自定义的类加载器给加载了。
五、双亲委派机制能被破坏吗
从上面的自定义类加载器的内容中,我们应该可以猜到了,破坏双亲委派直接重写loadClass方法就完事了。事实上,我们确实可以重写loadClass方法,毕竟这个方法没有被final修饰。双亲委派既然有好处,为什么jdk对loadClass开放重写呢?这要从双亲委派引入的时间来看:
双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,jdk为了向前兼容,不得已开放对loadClass的重写操作。
当然,破坏也不止这一次,jdbc与tomcat也破坏了双亲委派。
六、JDBC对双亲委派的破坏
还记得,我们第一次学jdbc的时候,是怎么连接数据库的吗?
先引用一个mysql-connector-java的jar包,这里的版本是5.0.8
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.0.8</version>
</dependency>
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");
这段代码,真的是勾起了我好多回忆啊~ 想起了那年在夕阳下奔跑的时光,那是我逝去的青春
首先要说明的是,该版本的jdbc并没有去打破双亲委派,或者说jdbc4.0前没有破坏双亲委派。
数据库这么多,jdk为了统一管理数据库驱动,在java.sql下定义了Driver接口,具体的实现由数据库厂商去做。
mysql对Driver接口的实现类是com.mysql.jdbc.Driver类,位于我们新引入的jar包中。
我们进入Class.forName中,发现最终会使用应用类加载器去加载com.mysql.jdbc.Driver类。
而该Driver位于引入的jar包中,确实是应该被应用类加载器加载。
接着进入到com.mysql.jdbc包下的Driver类中,它实现了rt.jar中的java.sql.Driver接口。
Class.forName会初始化该类,初始化的时候会执行静态方法。
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
//将mysql的Driver注册进驱动管理器中
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
所以,整个过程是:
- Class.forName会使用应用类加载器加载Driver实现类
- 加载Driver实现类需要执行静态方法,即将mysql的Driver注册进驱动管理器中,那么此时需要加载DriverManager类
- 应用类加载器去加载DriverManager类,而DriverManager位于rt.jar中,便一直向上委托到启动类加载器完成加载
这个过程确实没有破坏双亲委派
那么jdbc4.0后的情况呢?
为了使用该特性,我们需要引入高版本的mysql-connector-java,这里引入的版本是5.1.8
此时完全可以抛弃第一行的Class.forName语句了,使用以下语句来进行实验
Enumeration<Driver> en = DriverManager.getDrivers();
while (en.hasMoreElements()) {
java.sql.Driver driver = en.nextElement();
System.out.println(driver);
}
输出为:
看来内存中已经存在mysql的Driver了,这到底是怎么做的呢?
应用类加载器逐层委托到启动类加载器去加载DriverManager时,会同时执行它的静态方法
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
loadInitialDrivers内部核心的代码这有这两句
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
看到ServiceLoader,大家想到了什么,这不是jdk spi机制的核心吗?
spi机制在我的这篇文章SpringBoot的自动装配原理、自定义starter与spi机制,一网打尽有详细的一个介绍,并且对比了SpringBoot与JDK中spi机制的异同。
既然使用到了spi机制,那么mysql-connector-java的jar包在META-INF目录下必然有services目录,内容如下。
启动类加载DriverManager,之后需要通过spi机制去加载jar包中的Driver类,而该Driver理应被应用类加载器加载,这个时候就需要启动类加载器去通知应用类加载器,这明显违背了双亲委派机制。
那么,启动类加载器是怎么去通知应用类加载器的呢?
我们继续进入到ServiceLoader.load方法中
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Thread.currentThread().getContextClassLoader()是线程上下文类加载器,看来最终使用的是线程上下文类加载器去加载的Driver实现类。
而在sun.misc.Launcher类中,将应用类加载器设置进了线程上下文类加载器中,所以可以理解为,通过线程上下文类加载器,我们可以拿到应用类加载器的引用。
public Launcher() {
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
Thread.currentThread().setContextClassLoader(this.loader);
}
在jdbc4.0的情况下,梳理一下整个过程:
- 应用类加载器逐层委托到启动类加载器去加载DriverManager类
- 启动类加载器加载DriverManager类时,会执行其静态方法,即通过spi机制去加载jar包中的Driver实现类
- 此时启动类加载器需要委托应用类加载器加载Driver实现类,具体做法是通过线程上下文类加载器拿到应用类加载器的引用
确实是破坏了双亲委派!
七、Tomcat对双亲委派的破坏
tomcat有两个最基础的知识点,一个是应用打包放在webapps目录下就可以运行,另外一个是修改jsp会实时生效。
那这里抛出几个问题,来猜想一下Tomcat中类加载器的一个结构。
(1)jsp实时生效是怎么做的?
首先,在jvm中,如何去确定类的唯一性呢?是由类加载器实例+全限定名一起确定的。全限定名相同,类加载器不同,则会被认定为不同的类。
jsp文件被修改后,会被重新编译成Servlet,全限定名肯定是不变的,如果这个时候不去卸载加载该Servlet的类加载器,那么新jsp是无论如何都不会被加载进来的。因此,我们可以得知,每一个jsp文件都会对应一个类加载器实例。
(2)每个webapps下的应用依赖的类库是否会互相影响?
显然是不会影响的。应用A依赖低版本的Spring,而应用B依赖高版本的Spring,都是允许的。虽然Spring的版本不同,但某些类的全限定名是完全一致的。如果应用A与应用B采用同一个类加载器,是不会允许Spring版本不一样的。这里,我们猜想webapps下的每一个应用都会对应一个不同的类加载器实例,用以保持应用间的隔离。
从以上的两个问题,我们可以了解到:每一个jsp(或者说servlet)都对应一个不同的类加载器实例,每个webapp应用也是。
其实,tomcat5版本(以下如果没有另外声明版本,那么都是以该版本为例)的类加载器结构为:
其中各个加载器加载的范围为:
- Common ClassLoader:主要加载common目录下的资源
- Catalina ClassLoader:主要加载server目录下的资源
- Shared ClassLoader:主要加载shared目录下的资源
- Webapp ClassLoader:每一个应用会对应与该类型的一个实例,主要加载该应用下的WEB-INF下的资源
- JasperLoader:每一个jsp文件会对应于该类型的一个实例,就是为了修改jsp能及时生效
(前三个类加载器在tomcat6中已经合并了,合并之后的加载器加载lib目录下的资源)
它们在Bootstrap类中有过声明:
protected ClassLoader commonLoader = null;
protected ClassLoader catalinaLoader = null;
protected ClassLoader sharedLoader = null;
private void initClassLoaders() {
try {
commonLoader = createClassLoader("common", null);
if( commonLoader == null ) {
// no config file, default to this loader - we might be in a 'single' env.
commonLoader=this.getClass().getClassLoader();
}
catalinaLoader = createClassLoader("server", commonLoader);
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
其中createClassLoader方法会从container\\catalina\\src\\conf\\catalina.properties配置中读取每个加载器加载的范围:
common.loader=${catalina.home}/common/classes,${catalina.home}/common/i18n/*.jar,${catalina.home}/common/endorsed/*.jar,${catalina.home}/common/lib/*.jar
server.loader=${catalina.home}/server/classes,${catalina.home}/server/lib/*.jar
shared.loader=${catalina.base}/shared/classes,${catalina.base}/shared/lib/*.jar
catalina.home是安装目录,catalina.base是每个tomcat实例的工作目录。在只用一个tomcat的情况下,两个目录是一样的。
在了解了加载器的类型与范围之后,那么tomcat到底是怎么打破双亲委派机制的呢?
前面说过,双亲委派机制被定义在ClassLoader中的loadClass方法中,如果某个自定义的类加载想要打破双亲委派,那么重新loadClass方法即可。
Tomcat中的WebappClassLoader就是自定义类加载器,它的loadClass方法为:
public Class loadClass(String name) throws ClassNotFoundException {
return (loadClass(name, false));
}
public Class loadClass(String name, boolean resolve)
throws ClassNotFoundException {
if (log.isDebugEnabled())
log.debug("loadClass(" + name + ", " + resolve + ")");
Class clazz = null;
// Log access to stopped classloader
if (!started) {
try {
throw new IllegalStateException();
} catch (IllegalStateException e) {
log.info(sm.getString("webappClassLoader.stopped", name), e);
}
}
//1、从自己的本地缓存中查找,本地缓存的数据结构为ResourceEntry
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//2、从jvm的缓存中查找
clazz = findLoadedClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return (clazz);
}
//3、如果缓存中都找不到,则利用系统类加载器加载
try {
clazz = system.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
// Ignore
}
if (securityManager != null) {
int i = name.lastIndexOf('.');
if (i >= 0) {
try {
securityManager.checkPackageAccess(name.substring(0,i));
} catch (SecurityException se) {
String error = "Security Violation, attempt to use " +
"Restricted Class: " + name;
log.info(error, se);
throw new ClassNotFoundException(error, se);
}
}
}
boolean delegateLoad = delegate || filter(name);
//4、开启代理的话,则使用父加载器加载
if (delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader1 " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
//5、自行加载
if (log.isDebugEnabled())
log.debug(" Searching local repositories");
try {
clazz = findClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from local repository");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
//如果自己也加载不了,那就只能让父加载器加载了
if (!delegateLoad) {
if (log.isDebugEnabled())
log.debug(" Delegating to parent classloader at end: " + parent);
ClassLoader loader = parent;
if (loader == null)
loader = system;
try {
clazz = loader.loadClass(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Loading class from parent");
if (resolve)
resolveClass(clazz);
return (clazz);
}
} catch (ClassNotFoundException e) {
;
}
}
throw new ClassNotFoundException(name);
}
loadClass内部的逻辑整理如下:
- 先从WebappClassLoader的ResourceEntry缓存中查找
- 从jvm缓存中查找,比如去元数据区查找
- 利用系统类(应用类)加载器加载,避免webapp中的类覆盖掉标准类库中的类。
- 开启代理的话,则使用父加载器加载,这个默认没开启的。
- webappClassLoader自行去加载
- 自己也没加载成功的话,最后只能让父加载器去加载
这里有一个问题,对于一些非基础类库,为什么要先让webappClassLoader先去加载呢?
假设应用a依赖1.0版本的x.jar,而应用b依赖2.0版本的x.jar。为了保证两个应用的隔离性,首先要做的就是保证两个应用各自对应不同的webappClassLoader实例。如果这两个webappClassLoader实例在加载x.jar的时候,直接向上委托,那么最终只会加载一个版本的x.jar。
从上面,我们可以了解到:
对于一些标准类库中的类,比如Object类,会让系统类加载器加载,然后一直委托到启动类加载器,这个过程是没有违背双亲委派的。
而对于webapp中独有的类,则是webappClassLoader自行去加载,加载失败才让父加载器加载,明显是违背双亲委派的。
八、总结
双亲委派机制,核心是子加载器委托父加载器,能够避免java核心类库被篡改,增加了安全性。
但发展会带来创新,创新就会带来变革,jdbc与tomcat打破了这个自古相传的机制。
在jdbc中,父加载器委托子加载器。即利用线程上下文类加载器,让启动类加载器得以委托应用类加载器,去加载jar中的数据库驱动。
在tomcat中,子加载器优先于父加载器加载。即为了实现各个webapp的隔离性,webappClassLoader会先于父加载器加载。
以上是关于深度思考:老生常谈的双亲委派机制,JDBCTomcat是怎么反其道而行之的?的主要内容,如果未能解决你的问题,请参考以下文章