上一章我们讲到了类加载器和双亲委派机制的一些原理,对于双亲委派机制,我们也了解了双亲委派机制有沙箱安全机制和避免类的重复加载两大优点,这一章我们来讲述为什么要打破双亲委派机制以及如何打破双亲委派机制。并通过一些案例详细讲述打破双亲委派。
关于双亲委派机制,上一章有详细解释,其原理总结成一句话就是是:先委托给父亲加载,不行再派发给儿子自己加载。而对于双亲委派机制来说,有以下两大好处:
基于以上两大好处,可以看出双亲委派机制在安全和高效方面卓有成效,但是大家也知道Tomcat和SPI机制等都有打破双亲委派的操作,于是就有了以下疑问:
从上文可以知道,双亲委派机制虽然在安全和高效方面卓有成效,但是在一些特殊的场景下,我们不得不采取一些措施打破双亲委派机制,以下为一些打破双亲委派的案例:
对于双亲委派机制,我在上一篇文章《Java类加载器和双亲委派机制详解》中有提到有关双亲委派机制的代码在ClassLoader中的loadClass方法中,所以只需要继承ClassLoader、重写loadClass方法、修改双亲委派部分源码,就能打破双亲委派机制。
public class MyClassLoader extends ClassLoader {
private final String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写类加载方法,实现自己的加载逻辑,不委派给双亲加载
*
* @param name 类的二进制名称
* @param resolve 是否需要解决该类,一般为false
* @return 二进制名称(binary name)对应的Class对象
* @throws ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载。这点不变
synchronized (getClassLoadingLock(name)) {
// 首先,先检查类是否已被加载,避免重复加载。这点不变
Class<?> c = findLoadedClass(name);
// 如果没找到,通过findClass加载。这点不变
if (c == null) {
long t1 = System.nanoTime();
/*
* 重点
* 该处删除了委托parent(父加载器)加载的过程
* 直接通过自定义findClass方法加载
*
*/
c = findClass(name);
// ------------- 以下为JDK8原逻辑,删除部分时间计算逻辑 --------------
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadByte(String name) throws Exception {
// 替换为实际地址
name = name.replaceAll("\\.", "/");
FileInputStream fis = null;
byte[] data = null;
try {
// 加载class文件
fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
data = new byte[len];
fis.read(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 加载class文件
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}
首先,我们来考虑:
作为一个web容器,Tomcat要解决什么问题:
一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。
web容器也有自己依赖的类库,不能与应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。
web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情, web容器需要支持 jsp 修改能够热加载。
对于这些问题,我们不禁有了疑问:
Tomcat 如果使用默认的双亲委派类加载机制行不行?
问题1,如果同一个第三方类库的不同版本,意味着类名和加载路径也大概率相同,默认加载器对于同一类只会加载一次
问题2和问题3,默认加载器可以实现,因为他的职责就是保证唯一性。
问题4,如果使用默认加载器,类加载器会直接取方法区中已经存在的,并不会重新加载。这时候就要考虑为每一个jsp文件单独创建一个ClassLoader,每次更新jsp文件后,卸载之前的ClassLoader,重新加载。
Tomcat的ClassLoader结构:
可以看到CommonClassLoader、CatalinaClassLoader、SharedClassLoader是Tomcat自己定义的类加载器,它们分别加载/common/*
、/server/*
、/shared/*
(在tomcat 6之后已经合并到根目录下的lib目录下)和/WebApp/WEB-INF/*
中的Java类库。其中WebAppClassLoader和JasperLoader类加载器通常会存在多个实例,每一个Web应用程序对应一个WebAppClassLoader,每一个JSP文件对应一个JasperLoader。
从图中的委派关系中可以看出:
CommonClassLoader能加载的类都可以被CatalinaClassLoader和SharedClassLoader使用,从而实现了公有类库的共用,而CatalinaClassLoader和SharedClassLoader自己能加载的类则与对方相互隔离。
WebAppClassLoader可以使用SharedClassLoader加载到的类,但各个WebAppClassLoader实例之间相互隔离。
而JasperLoader的加载范围仅仅是这个JSP文件所编译出来的那一个.class文件,它出现的目的就是为了被丢弃:当Web容器检测到jsp文件被修改时,会替换掉目前的JasperLoader的实例,并通过再建立一个新的Jsp类加载器来实现JSP文件的热加载功能。
tomcat 这种类加载机制违背了java 推荐的双亲委派模型了吗?
答案是:违背了。
很显然,tomcat 不是这样实现,tomcat 为了实现隔离性,没有遵守这个约定,每个webappClassLoader加载自己的目录下的class文件,不会传递给父类加载器,打破了双亲委派机制。
对于WebappClassLoader,我们知道,不同的war包有不同的WebappClassLoader加载不同版本的依赖,也有部分需要用到SharedLoader相关依赖。我们就模拟这一过程。
public class MyClassLoader extends ClassLoader {
private final String classPath;
public MyClassLoader(String classPath) {
this.classPath = classPath;
}
/**
* 重写类加载方法,模拟Tomcat打破双亲委派过程。
* 对特定类自己加载,其他类还是通过父加载器加载
*
* @param name 类的二进制名称
* @param resolve 是否需要解决该类,一般为false
* @return 二进制名称(binary name)对应的Class对象
* @throws ClassNotFoundException 如果类未被找到,会抛出ClassNotFoundException
*/
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException {
// 基于类的binary name,对相同名称的类加锁,防止多个线程重复加载。这点不变
synchronized (getClassLoadingLock(name)) {
// 首先,先检查类是否已被加载,避免重复加载。这点不变
Class<?> c = findLoadedClass(name);
// 如果没找到,通过findClass加载。这点不变
if (c == null) {
long t1 = System.nanoTime();
/*
* 重点
* 该处对于com.tomcat.webapp(只是模拟)包下的class自己加载
* 对于其他class文件还是委托父类加载
*/
if (!name.startsWith("com.tomcat.webapp")){
c = this.getParent().loadClass(name);
}else{
c = findClass(name);
}
// ------------- 以下为JDK8原逻辑,删除获取父加载器时间 --------------
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
private byte[] loadByte(String name) throws Exception {
// 替换为实际地址
name = name.replaceAll("\\.", "/");
FileInputStream fis = null;
byte[] data = null;
try {
// 加载class文件
fis = new FileInputStream(classPath + "/" + name + ".class");
int len = fis.available();
data = new byte[len];
fis.read(data);
} catch (Exception e) {
e.printStackTrace();
} finally {
if (fis != null) {
fis.close();
}
}
return data;
}
protected Class<?> findClass(String name) throws ClassNotFoundException {
try {
// 加载class文件
byte[] data = loadByte(name);
//defineClass将一个字节数组转为Class对象,这个字节数组是class文件读取后最终的字节数组。
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
throw new ClassNotFoundException();
}
}
}