双亲委派模型
对于JVM类加载器来说,其实就是如下的code,JDK提供的三个类加载器,每个类加载器都加载自己范围内的类。Boot\EXT\APP 三个。双亲委派一句话就是,先让老爸处理,老爸处理不了,给爷爷。爷爷处理不了,自己在处理。
这样其实就会出现一个问题,越是基础的类,都有父类进行加载,出现两个java.lang.Object类,只会加载核心包的。不会加载用户自己的类。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 检查请求的类是否已经加载过了
Class<?> c = findLoadedClass(name);
// 如果加载过 跳过
// 没有加载过 执行
if (c == null) {
long t0 = System.nanoTime();
try {
// 父类不为空,先进行父类加载器加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果父加载器为空 使用跟加载器加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
// 如果抛出异常,说明父类加载器无法加载,找不到
}
if (c == null) {
// 父类加载器无法加载的时候,调用本身的加载器进行加载
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
tomcat如何设计类加载器
对于tomcat来说,可能有几个方面
- tomcat运行两个web应用,当有相同的包名+类名,应该需要隔离。
- 不同的Web应用程序,当出现相同的依赖包,应该统一加载。
- 隔离tomcat 和 Web应用本身的类
好了带着这三个问题,来思考看tomcat是如何解决的
WebAppClassLoader
第一个问题:当在tomcat下部署多个应用时,同时出现. com.qxlx.StudentServlet的类,应该进行区分,对于类加载器来说不同的类加载器加载相同的全限定类名认为是不同的类,所以针对每个web应用创建一个单独的classLoader。也即WebAppClassLoader,每个Web应用有自己的类空间,Web应用之间可以通过各自的类加载器进行隔离。
SharedClassLoader
第二个问题:多个Web应用之间怎么进行共享相同的库,对于双亲委派模型来说,就是通过父类进行加载最基础的类,因此只需要在webAppClassLoader进行设置一个父类加载器,SharedClassLoader 专门加载Web应用之间共享的类,当WebAppClassLoader没有加载这个类,就委托给SharedClassLoader去加载。
catalinaClassLoader
第三个问题:也就是web应用和tomcat进行隔离,我们知道共享需要继承,隔离就需要兄弟关系。因此,需要在SharedClassLoader 创建一个兄弟类加载器,CatalinaClassLoader。加载tomcat基础类,但是这样有问题,就是tomcat和web应用共享的类如何加载。
其实就是在加一层父类加载器,CommonClassLoader
CatalinaClassLoader加载基础的类,因此最终就是如下图。
那么tomcat是如何进行设置类加载器的,源码之下无秘密。可以看到,先创建一个common,然后catalina和shared 并设置父类加载器为common.
private void initClassLoaders() {
try {
//创建common类加载器
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,父类加载器为commonLoader
catalinaLoader = createClassLoader("server", commonLoader);
// 创建sharedLoader,父类加载器为commonLoader
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
并且设置为当前线程类加载器为catalinaLoader。由于bootStrap调用的是catalina的init 和 start() ,通过反射常见catalina对象,并设置父类加载器为 shardloader
// 设置上下文类加载器 catalinaLoader 加载tomcat专用的类
Thread.currentThread().setContextClassLoader(catalinaLoader);
// catalinaLoader 加载 Catalina 类
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance();
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
// 设置catalina类的parentClassLoader属性为shardloader
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues);
每个web应用其实就是一个context , context的实现StandardContext 可以发现为每个web应用创建一个单独的类加载器,这样就可以加载了。
// 为每个应用设置类加载器
if (getLoader() == null) {
WebappLoader webappLoader = new WebappLoader();
webappLoader.setDelegate(getDelegate());
setLoader(webappLoader);
}
源码
public Class<?> findClass(String name) throws ClassNotFoundException {
...
Class<?> clazz = null;
try {
//1. 先在Web应用目录下查找类
clazz = findClassInternal(name);
} catch (RuntimeException e) {
throw e;
}
if (clazz == null) {
try {
//2. 如果在本地目录没有找到,交给父加载器去查找
clazz = super.findClass(name);
} catch (RuntimeException e) {
throw e;
}
//3. 如果父类也没找到,抛出ClassNotFoundException
if (clazz == null) {
throw new ClassNotFoundException(name);
}
return clazz;
}
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> clazz = null;
//1. 先在本地cache查找该类是否已经加载过
clazz = findLoadedClass0(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
//2. 从系统类加载器的cache中查找是否加载过
clazz = findLoadedClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
// 3. 尝试用ExtClassLoader类加载器类加载,为什么?
ClassLoader javaseLoader = getJavaseClassLoader();
try {
clazz = javaseLoader.loadClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 4. 尝试在本地目录搜索class并加载
try {
clazz = findClass(name);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
// 5. 尝试用系统类加载器(也就是AppClassLoader)来加载
try {
clazz = Class.forName(name, false, parent);
if (clazz != null) {
if (resolve)
resolveClass(clazz);
return clazz;
}
} catch (ClassNotFoundException e) {
// Ignore
}
}
//6. 上述过程都加载失败,抛出异常
throw new ClassNotFoundException(name);
}
核心流程,其实就是先查找加载过没有,没有加载过,然后先尝试用EXT进行加载,避免覆盖JRE核心类。然后才是本地加载 以及app加载。
小结
本篇主要介绍tomcat是如何打破双亲委派模型,以及相关的源码流程,其实在学习的过程中,源码更多的是是解决问题时的一个落地实现,我们需要掌握背后设计的思路更关键。