通过前面的文章我们知道,Tomcat的请求最终都会交给用户配置的servlet实例来处理。Servlet类是配置在配置文件中的,这就需要类加载器对Servlet类进行加载。Tomcat容器自定义了类加载器,有以下特殊功能:1. 在载入类中指定某些规则;2.缓存已经载入的类;3.实现类的预加载。 本文会对Tomcat的类加载器进行详细介绍。

Java类加载双亲委派模型

Java类加载器是用户程序和JVM虚拟机之间的桥梁,在Java程序中起了至关重要的作用,关于其详细实现可以参考了java官方文档关于虚拟机加载的教程,点此直达官方参考文档。java中的类加载默认是采用双亲委派模型,即加载一个类时,首先判断自身define加载器有没有加载过此类,如果加载了直接获取class对象,如果没有查到,则交给加载器的父类加载器去重复上面过程。我在另外一篇文章中详细介绍了Java的类加载机制,此处不做详细介绍。

Loader接口

在载入Web应用程序中需要的servlet类及其相关类时要遵守一些明确的规则,例如应用程序中的servlet只能引用部署在WEB-INF/classes目录及其子目录下的类。但是,servlet类不能访问其它路径中的类,即使这些累包含在运行当前Tomcat的JVM的CLASSPATH环境变量中。此外,servlet类只能访问WEB-INF/LIB目录下的库,其它目录的类库均不能访问。Tomcat中的载入器值得是Web应用程序载入器,而不仅仅是类载入器,载入器必须实现Loader接口。Loader接口的定义如下所示:

public interface Loader {

    public void backgroundProcess();
public ClassLoader getClassLoader();
public Context getContext();
public void setContext(Context context);
public boolean getDelegate();
public void setDelegate(boolean delegate);
public void addPropertyChangeListener(PropertyChangeListener listener);
public boolean modified();
public void removePropertyChangeListener(PropertyChangeListener listener);
}

后台任务:Loader接口需要进行在servlet类变更的时候实现类的重新加载,这个任务就是在backgroundProcess()中实现的,WebApploader中backgroundProcess()的实现如下所示。可以看到,当Context容器开启了Reload功能并且仓库变更的情况下,Loaders会先把类加载器设置为Web类加载器,重启Context容器。重启Context容器会重启所有的子Wrapper容器,会销毁并重新创建servlet类的实例,从而达到动态加载servlet类的目的。

    @Override
public void backgroundProcess() {
Context context = getContext();
if (context != null) {
if (context.getReloadable() && modified()) {
ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
context.reload();
} finally {
Thread.currentThread().setContextClassLoader(originalTccl);
}
}
}
}

类加载器:Loader的实现中,会使用一个自定义类载入器,它是WebappClassLoader类的一个实例。可以使用Loader接口的getClassLoader()方法来获取Web载入器中的ClassLoader的实例。默认的类加载器的实现有两种种:ParallelWebappClassLoader和WebappClassLoader

Context容器:Tomcat的载入器通常会与一个Context级别的servelt容器相关联,Loader接口的getContainer()方法和setContainer()方法用来将载入器和某个servlet容器关联。如果Context容器中的一个或者多个类被修改了,载入器也可以支持对类的重载。这样,servlet程序员就可以重新编译servlet类及其相关类,并将其重新载入而不需要重新启动Tomcat。Loader接口使用modified()方法来支持类的自动重载。

类修改检测:在载入器的具体实现中,如果仓库中的一个或者多个类被修改了,那么modified()方法必须放回true,才能提供自动重载的支持

父载入器:载入器的实现会指明是否要委托给父类的载入器,可以通过setDelegate()和getDelegate方法配置。

WebappLoader类

Tomcat中唯一实现Loader接口的类就是WebappLoader类,其实例会用作Web应用容器的载入器,负责载入Web应用程序中所使用的类。在容器启动的时候,WebApploader会执行以下工作:

  • 创建类加载器
  • 设置仓库
  • 设置类的路径
  • 设置访问权限
  • 启动新线程来支持自动重载

创建类加载器

为了完成类加载功能,WebappLoader会按照配置创建类加载器的实例,Tomcat默认有两种类加载器:WebappClassLoader和ParallelWebappClassLoader,默认情况下使用ParallelWebappClassLoader作为类加载器。用户可以通过setLoaderClass()设置类加载器的名称。WebappLoader创建类加载器的源码如下所示,我们可以看到类加载器的实例必须是WebappClassLoaderBase的子类。

    private WebappClassLoaderBase createClassLoader()
throws Exception { if (classLoader != null) {
return classLoader;
} if (ParallelWebappClassLoader.class.getName().equals(loaderClass)) {
return new ParallelWebappClassLoader(context.getParentClassLoader());
} Class<?> clazz = Class.forName(loaderClass);
WebappClassLoaderBase classLoader = null; ClassLoader parentClassLoader = context.getParentClassLoader(); Class<?>[] argTypes = { ClassLoader.class };
Object[] args = { parentClassLoader };
Constructor<?> constr = clazz.getConstructor(argTypes);
classLoader = (WebappClassLoaderBase) constr.newInstance(args); return classLoader;
}

设置仓库

WebappLoader会在启动的时候调用类加载器的初始化方法,类加载器在初始化的时候会设置类加载的仓库地址。默认的仓库地址为"/WEB-INF/classes"和"/WEB-INF/lib"。类加载器初始化源码如下所示:

    @Override
public void start() throws LifecycleException { state = LifecycleState.STARTING_PREP; WebResource[] classesResources = resources.getResources("/WEB-INF/classes");
for (WebResource classes : classesResources) {
if (classes.isDirectory() && classes.canRead()) {
localRepositories.add(classes.getURL());
}
}
WebResource[] jars = resources.listResources("/WEB-INF/lib");
for (WebResource jar : jars) {
if (jar.getName().endsWith(".jar") && jar.isFile() && jar.canRead()) {
localRepositories.add(jar.getURL());
jarModificationTimes.put(
jar.getName(), Long.valueOf(jar.getLastModified()));
}
} state = LifecycleState.STARTED;
}

设置类路径

设置类路径是在初始化的时候调用setClassPath()方法完成的(源码如下)。setClassPath()方法会在servlet上下文中为Jasper JSP编译器设置一个字符串类型的属性来指明类路径信息。此处不详细介绍JSP相关内容。

  private void setClassPath() {

        // Validate our current state information
if (context == null)
return;
ServletContext servletContext = context.getServletContext();
if (servletContext == null)
return; StringBuilder classpath = new StringBuilder(); // Assemble the class path information from our class loader chain
ClassLoader loader = getClassLoader(); if (delegate && loader != null) {
// Skip the webapp loader for now as delegation is enabled
loader = loader.getParent();
} while (loader != null) {
if (!buildClassPath(classpath, loader)) {
break;
}
loader = loader.getParent();
} if (delegate) {
// Delegation was enabled, go back and add the webapp paths
loader = getClassLoader();
if (loader != null) {
buildClassPath(classpath, loader);
}
} this.classpath = classpath.toString(); // Store the assembled class path as a servlet context attribute
servletContext.setAttribute(Globals.CLASS_PATH_ATTR, this.classpath);
}

设置访问权限

若是运行Tomcat的时候,使用了安全管理器,则setPermissions()方法会为类载入器设置访问相关目录的权限,比如只能访问WEB-INF/classes和WEB-INF/lib的目录。若是没有使用安全管理器,则setPermissions()方法只是简单地返回,什么也不做。其源码如下:


/**
* Configure associated class loader permissions.
*/
private void setPermissions() { if (!Globals.IS_SECURITY_ENABLED)
return;
if (context == null)
return; // Tell the class loader the root of the context
ServletContext servletContext = context.getServletContext(); // Assigning permissions for the work directory
File workDir =
(File) servletContext.getAttribute(ServletContext.TEMPDIR);
if (workDir != null) {
try {
String workDirPath = workDir.getCanonicalPath();
classLoader.addPermission
(new FilePermission(workDirPath, "read,write"));
classLoader.addPermission
(new FilePermission(workDirPath + File.separator + "-",
"read,write,delete"));
} catch (IOException e) {
// Ignore
}
} for (URL url : context.getResources().getBaseUrls()) {
classLoader.addPermission(url);
}
}

开启新线程执行类的重新载入

WebappLoader类支持自动重载功能。如果WEB-INF/classes目录或者WEB-INF/lib目录下的某些类被重新编译了,那么这个类会自动重新载入,而无需重启Tomcat。为了实现此目的,WebappLoader类使用一个线程周期性的检查每个资源的时间戳。间隔时间由变量checkInterval指定,单位为s,默认情况下,checkInterval的值为15s,每隔15s会检查依次是否有文件需要自动重新载入。顶层容器在启动的时候,会启动定时线程池循环调用backgroundProcess任务。

    protected void threadStart() {
if (backgroundProcessorDelay > 0
&& (getState().isAvailable() || LifecycleState.STARTING_PREP.equals(getState()))
&& (backgroundProcessorFuture == null || backgroundProcessorFuture.isDone())) {
if (backgroundProcessorFuture != null && backgroundProcessorFuture.isDone()) {
// There was an error executing the scheduled task, get it and log it
try {
backgroundProcessorFuture.get();
} catch (InterruptedException | ExecutionException e) {
log.error(sm.getString("containerBase.backgroundProcess.error"), e);
}
}
backgroundProcessorFuture = Container.getService(this).getServer().getUtilityExecutor()
.scheduleWithFixedDelay(new ContainerBackgroundProcessor(),
backgroundProcessorDelay, backgroundProcessorDelay,
TimeUnit.SECONDS);
}
} @Override
public void backgroundProcess() {
Context context = getContext();
if (context != null) {
if (context.getReloadable() && modified()) {
ClassLoader originalTccl = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());
context.reload();
} finally {
Thread.currentThread().setContextClassLoader(originalTccl);
}
}
}
}

WebappClassLoader类加载器

Web应用程序中负责载入类的类载入器有两种:ParallelWebappClassLoader和WebappClassLoaderBase,二者实现大同小异,本节以WebappClassLoader类加载器为例,介绍Tomcat的类加载器。

WebappClassLoader的设计方案考虑了优化和安全两方面。例如,它会缓存之前已经载入的类来提升性能,还会缓存加载失败的类的名字,这样,当再次请求加载同一个类的时候,类加载器就会直接抛出ClassNotFindException异常,而不是再次去查找这个类。WebappClassLoader会在仓库列表和指定的JAR文件中搜索需要在载入的类。

类缓存

为了达到更好的性能,WebappClassLoader会缓存已经载入的类,这样下次再使用该类的时候,会直接从缓存中获取。由WebappClassLoader载入的类都会被视为资源进行缓存,对应的类为“ResourceEntry”类的实例。ResourceEndty保存了其所代表的class文件的字节流、最后一次修改日期,Manifest信息等。如下为类加载过程中读取缓存的部分代码和ResourceEntry的定义源码。

public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {

    // 省略部分逻辑
// (0) Check our previously loaded local class cache
clazz = findLoadedClass0(name);
if (clazz != null) {
if (log.isDebugEnabled())
log.debug(" Returning class from cache");
if (resolve)
resolveClass(clazz);
return clazz;
}
// 省略部分逻辑
} protected Class<?> findLoadedClass0(String name) { String path = binaryNameToPath(name, true); ResourceEntry entry = resourceEntries.get(path);
if (entry != null) {
return entry.loadedClass;
}
return null;
} public class ResourceEntry {
/**
* The "last modified" time of the origin file at the time this resource
* was loaded, in milliseconds since the epoch.
*/
public long lastModified = -1; /**
* Loaded class.
*/
public volatile Class<?> loadedClass = null;
}

载入类

载入类的时候,WebappClassLoader要遵循如下规则:

  1. 因为所有已经载入的类都会缓存起来,所以载入类的时候要先检查本地缓存。
  2. 若本地缓存没有,则检查父类加载器的缓存,调用ClassLoader接口的findLoadedClass()方法。
  3. 若两个缓存总都没有,则使用系统类加载器进行加载,防止Web应用程序中的类覆盖J2EE中的类。
  4. 若启用了SecurityManager,则检查是否允许载入该类。若该类是禁止载入的类,抛出ClassNotFoundException异常。
  5. 若打开了标志位delegate,或者待载入的在类不能用web类加载器加载的类,则使用父类加载器来加载器来加载相关类。如果父类加载器为null,则使用系统类加载器。
  6. 从当前仓库载入类。
  7. 当前仓库没有需要载入的类,而且delegate关闭,则是用父类载入器来载入相关的类。
  8. 若没有找到需要加载的类,则抛出ClassNotFindException。

Tomcat类加载结构

Tomcat容器在启动的时候会初始化类加载器,Tomcat的类加载器分为四种类型:Common类加载器,Cataline类加载器和Shared类加载器,此外每个应用都会有自己的Webapp类加载器,也就是我们上文介绍的WebappClassLoader,四者之间的关系如下所示。

Common类加载器,Cataline类加载器和Shared类加载器会在Tomcat容器启动的时候就初始化完成,初始化代码如下所示:

    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) {
handleThrowable(t);
log.error("Class loader creation threw exception", t);
System.exit(1);
}
} private ClassLoader createClassLoader(String name, ClassLoader parent)
throws Exception { String value = CatalinaProperties.getProperty(name + ".loader");
if ((value == null) || (value.equals("")))
return parent; value = replace(value); List<Repository> repositories = new ArrayList<>(); String[] repositoryPaths = getPaths(value); 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));
}
} return ClassLoaderFactory.createClassLoader(repositories, parent);
}

而Webapp类加载器则是在Context容器启动时候有WebappLoader初始化,Webapp类加载器的父类加载器是Tomcat容器在初始化阶段通过反射设置的,反射设置父类加载器的源码如下所示:

    public void init() throws Exception {

        initClassLoaders();

        Thread.currentThread().setContextClassLoader(catalinaLoader);

        SecurityClassLoad.securityClassLoad(catalinaLoader);

        // Load our startup class and call its process() method
if (log.isDebugEnabled())
log.debug("Loading startup class");
Class<?> startupClass = catalinaLoader.loadClass("org.apache.catalina.startup.Catalina");
Object startupInstance = startupClass.getConstructor().newInstance(); // Set the shared extensions class loader
if (log.isDebugEnabled())
log.debug("Setting startup class properties");
String methodName = "setParentClassLoader";
Class<?> paramTypes[] = new Class[1];
paramTypes[0] = Class.forName("java.lang.ClassLoader");
Object paramValues[] = new Object[1];
paramValues[0] = sharedLoader;
Method method =
startupInstance.getClass().getMethod(methodName, paramTypes);
method.invoke(startupInstance, paramValues); catalinaDaemon = startupInstance;
}

Tomcat类加载结构的目的

  1. 一个web容器可能需要部署两个应用程序,不同的应用程序可能会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。所以每个应用需要自身的Webapp类加载器。
  2. 部署在同一个web容器中相同的类库相同的版本可以共享。否则,如果服务器有10个应用程序,那么要有10份相同的类库加载进虚拟机。所以需要Shared类加载器
  3. web容器也有自己依赖的类库,不能于应用程序的类库混淆。基于安全考虑,应该让容器的类库和程序的类库隔离开来。所以需要Cataline类加载器。
  4. web容器要支持jsp的修改,我们知道,jsp 文件最终也是要编译成class文件才能在虚拟机中运行,但程序运行后修改jsp已经是司空见惯的事情,否则要你何用? 所以,web容器需要支持 jsp 修改后不用重启。

还有最后一个类的共享的问题,如果十个web应用都引入了spring的类,由于web类加载器的隔离,那么对内存的开销是很大的。此时我们可以想到shared类加载器,我们肯定都会选择将spring的jar放于shared目录底下,但是此时又会存在一个问题,shared类加载器是webapp类加载器的parent,若spring中的getBean方法需要加载web应用底下的类,这种过程是违反双亲委托机制的。

打破双亲委托机制的桎梏:线程上下文类加载器线程上下文类加载器是指的当前线程所用的类加载器,可以通过Thread.currentThread().getContextClassLoader()获得或者设置。在spring中,他会选择线程上下文类加载器去加载web应用底下的类,如此就打破了双亲委托机制。

参考文档列表

我是御狐神,欢迎大家关注我的微信公众号:wzm2zsd

本文最先发布至微信公众号,版权所有,禁止转载!

学习Tomcat(六)之类加载器的更多相关文章

  1. 还是Tomcat,关于类加载器的趣味实验

    一.前言 类加载器,其实是很复杂一个东西,想等到我完全什么都弄明白了再写出来,估计不太现实...现在只能是知道多少写多少吧. 首先,我提一个问题:在我们自己的servlet中(比如ssm中,contr ...

  2. 深入理解JVM(六)——类加载器原理

    我们知道我们编写的java代码,会经过编译器编译成字节码文件(class文件),再把字节码文件装载到JVM中,映射到各个内存区域中,我们的程序就可以在内存中运行了.那么字节码文件是怎样装载到JVM中的 ...

  3. Tomcat内核之类加载器工厂

    Java虚拟机利用类加载器将类载入内存,以供使用.在此过程中类加载器要做很多的事情,例如读取字节数组.验证.解析.初始化等.而Java提供的URLClassLoader类能方便地将jar.class或 ...

  4. Java第三阶段学习(九、类加载器、反射)

    一.类加载器 1.类的加载: 当程序要使用某个类时,如果该类还未被加载到内存中,则系统会通过加载,连接,初始化三步来实现对这个类进行初始化. 1.1 加载: 就是指将class文件读入内存,并为之自动 ...

  5. Tomcat实现自定义类加载器

    什么是类加载器? 这是官方给的定义 在 Java 虚拟机的实现中,初始类可以作为命令行参数提供. 或者,该实现可以提供一个初始类,该类设置一个类加载器,该类加载器依次加载应用程序. 初始类的其他选择也 ...

  6. 【JVM学习笔记】系统类加载器

    可以通过“java.system.class.loader"属性指定系统类加载器 默认情况下,该属性值为空: public class Test { public static void m ...

  7. 【JVM学习笔记】扩展类加载器

    扩展类加载器独有的特点,代码如下 public class Sample { } public class Test { static { System.out.println("Test ...

  8. 4.自定义类加载器实现及在tomcat中的应用

    了解了类加载器的双亲委派机制, 也知道了双亲委派机制的原理,接下来就是检验我们学习是否扎实了,来自定义一个类加载器 一. 回顾类加载器的原理 还是这张图,类加载器的入口是c++调用java代码创建了J ...

  9. tomcat: 类加载器

    一.tomcat是个web容器,要解决以下问题 1. 一个web容器可能要部署两个或者多个应用程序,不同的应用程序,可能会依赖同一个第三方类库的不同版本,因此要保证每一个应用程序的类库都是独立.相互隔 ...

随机推荐

  1. CAS 的ABA 问题

    CAS CAS:Compare and Swap, 翻译成比较并交换. java.util.concurrent包中借助CAS实现了区别于synchronized同步锁的一种乐观锁. 其原理是CAS有 ...

  2. jQuery中的表单过滤选择器(四、七)::input、:text、:password、:radio、:checkbox、:file等

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <hea ...

  3. 手动设置IDEA失效的配置文件

  4. RHEL7.2下DHCP服务器的搭建

    DHCP(?Dynamic Host Configuration Protocol)是一种帮助计算机从指定的DHCP服务器获取配置信息的自举协议 请求配置信息的计算机叫做DHCP客户端,而提供信息的叫 ...

  5. 01_Keil与Proteus联合仿真的注意事项

    01. 关于keil5和Proteus8的联合仿真的操作步骤,这里就不细说,给个链接,步骤差不多是最齐全的 CSDN博客链接:https://blog.csdn.net/wzk456/article/ ...

  6. 矩阵BFS

    leetcode 1091矩阵BFS 在一个 N × N 的方形网格中,每个单元格有两种状态:空(0)或者阻塞(1). 一条从左上角到右下角.长度为 k 的畅通路径,由满足下述条件的单元格 C_1, ...

  7. Django的form组件——自定义校验函数

    from django.shortcuts import render,HttpResponse from django import forms from django.core.exception ...

  8. 20210716 noip17

    考场 终于有一场在晚上考了 T1 随便画了画就发现要求每个点的后继个数,想起来有 dfs 和 toposort 两种方法,感觉很稳 T2 裸的网络流有 70pts?!真香 一看 T3 就想起了 Mst ...

  9. noip模拟48

    A. Lighthouse 很明显的容斥题,组合式与上上场 \(t2\) 一模一样 注意判环时长度为 \(n\) 的环是合法的 B. Miner 题意实际上是要求偶拉路 对于一个有多个奇数点的联通块, ...

  10. Python习题集(十五)

    每天一习题,提升Python不是问题!!有更简洁的写法请评论告知我! https://www.cnblogs.com/poloyy/category/1676599.html 题目 请写一个函数,该函 ...