《Tomcat源码解析》Tomcat类加载器
《Tomcat源码》系列文章集合:
Tomcat结构设计
Tomcat启动过程
Tomcat类加载器
Tomcat的Web请求和处理过程
文章目录
前言
一、J2SE类加载器
JVM默认提供了3个类加载器,他们以一种父子树的方式创建,同时使用委派模式确保应用程序可通过自身的类加载器(System)加载所有可见的Java类。Jvm对类的初始化是一个延迟的机制,即使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行包下,一个Class只会被初始化一次。
1.1 启动类加载器
打印启动类加载器的加载路径:
1.2 扩展类加载器
打印扩展类加载器的加载路径
1.3 系统类加载器
应用程序不在自己构造类加载器的情况下,使用System作为默认的类加载器。如果应用程序自己构造类加载器,基本也以System作为父类加载器。
1.4 代码获取各种类加载器
1.5 双亲委派机制
双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,他首先不会自己去尝试加载这个类,而是把这个请求委派父类加载器去完成。每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个请求(他的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
(1)当自定义一个java.lang.String对象并且尝试在程序里面使用时,会发现程序不会调用自定义的String对象,而是调用jdk提供的String对象,是因为类加载时,会先让他的父类加载器先去加载,一直向上委托,直到启动类加载器,启动类加载器会加载java包下的类,所以String对象是jdk提供的对象。
(2)同理,如果想运行自己定义的java.lang.String的main方法,程序会报错,提示找不到main方法,因为jdk提供的String对象不存在main方法。
双亲委派机制的作用就是避免类的重复加载、保护程序安全,防止核心API被随意篡改
1.6 ClassLoader
ClassLoader的loadClass实现了双亲委派机制
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 1、首先从缓存中获取
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 2、缓存中没有,就从父类加载器从加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// parent == null,代表parent为BootStrap类加载器。
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// 如果还没有找到,就调用findClass加载类。
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类加载器
应用服务器通常会自行创建类加载器以实现更灵活的控制,这一方面是对规范的实现(Servelt规范要求每个Web应用都有一个独立的类加载器实例),另一方面也有架构层面的考虑。
(1)隔离性:Web应用类库相互隔离,避免依赖库或者应用包相互影响。
(2)灵活性:既然Web应用之间的类加载器相互独立,那么我们就能只针对一个Web应用进行重新部署,此时该Web应用的类加载器将会被重新创建,而且不会影响其他Web应用。
(3)性能:由于每个Web应用都有一个类加载器,因此Web应用在加载类时,不会搜索其他Web应用包含的Jar包,性能自然高于应用服务器只有一个类加载器的情况。
Tomcat类加载器方案:
除了每个Web应用的类加载器外,Tomcat也提供了3个基础的类加载器和Web应用类加载器,而且这三个类加载器指向的路径和包列表均可以有catalina.properties配置。
(1)Common:以System为父类加载器,是位于Tomcat应用服务器顶层的公用类加载器。其路径为common.loader,默认指向$CATALINA_HOME/lib下的包。
(2)Catalina:以Common为父加载器,适用于加载Tomcat应用服务器的类加载器,其路径为server.loader,默认为空。此时Tomcat使用Common类加载器应用服务器。
(3)Sharded:以Common为父加载器,是所有Web应用的父加载器,其路径是shared.loader,默认为空,此时Tomcat使用Common类加载器作为Web应用的父加载器。
(4)Web应用:以shared为父加载器,加载/WEB-INF/classes目录下的为压缩的Class和资源文件以及/WEB-INF/lib目录下的Jar包。该加载器只对当前Web应用可见,对其他Web应用不可见。
首先Common类加载器负责加载Tomcat应用服务器内部和Web应用均可见的类,例如Servelt规范相关的包和一些通用的工具包。
其次,Catalina类加载器负责加载只有Tomcat应用服务器内部可见的类,这些类对Web应用不可见,如Tomcat的具体实现类,因为Web应用最好与服务器松耦合,所以不应该依赖应用服务器内部的类。
再次Shared类加载器负责加载Web应用共享的类,这些类Tomcat服务器不会依赖。
如果我们想实现自己的会话存储方案,而且该方案依赖了一些第三方包,我们会不希望这些包会Web应用可见,此时我们可以配置server.loader,创建新的Catalina类加载器。
最后,Tomcat服务器$CATALINA_HOME/bin目录下的包作为启动入口由System类加载器加载。通过这几个启动包的隔离,Tomcat简化了应用服务器的启动,同时增加了灵活性。
三、Web应用类加载器
Java默认的委派机制的过程如下:
(1)从缓存中加载
(2)如果缓存中没有,则从父类加载器中加载
(3)如果父类加载器没有,则从当前类加载器加载。
(4)如果没有,则抛出异常
Tomcat提供的Web应用类加载器与默认的委派机制不同。当进行类加载器时,除了JVM基础类库外,它会首先尝试通过当前类加载器加载,然后才进行委派。Servelt规范相关API禁止通过Web应用类加载器加载,因此不要在Web应用中包含这些包。
Web应用类加载器默认的加载顺序如下:
(1)从缓存中加载
(2)如果没有,则从JVM的BootStrap类加载器加载。
(3)如果没有,则从当前类加载器加载(按照WEB-INF/classes、WEB-INF/lib的顺序)
(4)如果没有,则从当前父类加载器加载,由于父类加载器采用默认的委派模式,所以加载顺序为System、Common、Shared。
Tomcat提供了delegate属性用于控制是否启用Java委派模式,默认为false(不启用)。当配置为true时,Tomcat将使用Java默认的委派模式:
(1)从缓存中加载
(2)如果没有,从JVM的Bootstrap类加载器加载
(3)如果没有,从父类的加载器加载(System、Common、Shared)
(4)如果没有,则从当前类加载器加载。
除了可以通过delegate属性控制是否启用Java的委派模式外,Tomcat还可以通过packegeTriggerDeny属性只让某些包路径采用Java的委派模式,Web应用类加载器对于符合packegeTriggerDeny指定包路径的类强制采用Java的委派模式。
Tomcat通过该机制实现对Web应用中的Jar包覆盖服务器提供包的目的,Java核心类库、Servelt规范相关类库是无法覆盖的,此外Java默认提供的诸如XML工具包,由于位于Jvm的Bootstrap类加载器也无法覆盖,只能通过endorsed方式实现。
四、Tomcat类加载器源码
Tomcat类加载器的初始化是在Bootstrap的init方法。
// Tomcat的三个类加载器
ClassLoader commonLoader = null;
ClassLoader catalinaLoader = null;
ClassLoader sharedLoader = null;
// -------------------------------------------------------- Private Methods
// 初始化类加载器,tomcat自定义的类加载器
private void initClassLoaders() {
try {
// 1、创建CommonClassLoader;负责加载Web应用和服务器都可见的类
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();
}
// 2、创建CatalinaClassLoader;负责加载只有Tomcat应用服务器内部可见的类
catalinaLoader = createClassLoader("server", commonLoader);
// 3、创建SharedClassLoader;负责加载Web应用共享的类
sharedLoader = createClassLoader("shared", commonLoader);
} catch (Throwable t) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
}
private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception {
// 从CatalinaProperties里面加载 属性name + ".loader"
// Value: "${catalina.base}/lib" 用的是相对路径
String value = CatalinaProperties.getProperty(name + ".loader");
// 找不到该属性,就返回parent
if ((value == null) || (value.equals("")))
return parent;
//将相对路径转成绝对路径
value = replace(value);
List<Repository> repositories = new ArrayList<>();
// 将一个字符串形式的PATH路径分割成仓库数组
String[] repositoryPaths = getPaths(value);
// 遍历数组,封装Repository集合
for (String repository : repositoryPaths) {
// Check for a JAR URL repository
try {
@SuppressWarnings("unused")
URL url = new URL(repository);
repositories.add(new Repository(repository, RepositoryType.URL));
continue;
} catch (MalformedURLException e) {
// Ignore
}
// Local repository
if (repository.endsWith("*.jar")) {
repository = repository.substring
(0, repository.length() - "*.jar".length());
repositories.add(new Repository(repository, RepositoryType.GLOB));
} else if (repository.endsWith(".jar")) {
repositories.add(new Repository(repository, RepositoryType.JAR));
} else {
repositories.add(new Repository(repository, RepositoryType.DIR));
}
}
// 根据加载目录的集合 和 父加载器,创建类加载器对象。
// 在这个方法里面可以看到这三个类加载器都是URLClassLoader的子类。
return ClassLoaderFactory.createClassLoader(repositories, parent);
}