Tomcat 源码分析(三)-(三)-自动加载类及检测文件变动原理

2024-04-06 08:48

本文主要是介绍Tomcat 源码分析(三)-(三)-自动加载类及检测文件变动原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Tomcat 源码分析(三)-WEB加载原理(三)

文章目录

  • Tomcat 源码分析(三)-WEB加载原理(三)
    • @[toc]
    • Tomcat 7 自动加载类及检测文件变动原理
      • 关于开发工具中的自动加载
      • 分析Tomcat自动加载的实现
      • 检测文件变动分析
        • WebappLoader 的初始化
        • WebappClassLoader 的 modified 方法-检测变动的代码
        • 关于当前资源信息获取
        • 关于已加载类的资源信息
      • 结束
    • 参考资料
      • 源码分析三:web 应用加载原理

Tomcat 7 自动加载类及检测文件变动原理

关于开发工具中的自动加载

在常用的web应用开发工具(如 Eclipse、IntelJ )中都有集成Tomcat,这样可以将开发的web项目直接发布到tomcat中去。这里会遇到一种情况,在修改了一个文件后,开发工具可以直接编译class文件发布到tomcat的web工程里面。如果tomcat没有配置自动加载功能,JVM中的还是就的class,就需要手动进行restart。

所以,这里说一下tomcat提供的配置自动加载的配置属性:

`<Context path="/HelloWorld" docBase="C:/apps/apache-tomcat/DeployedApps/HelloWorld" reloadable="true"/>`

就是reloadable="true"这个属性,这样 Tomcat 就会监控所配置的 web 应用实际路径下的/WEB-INF/classes/WEB-INF/lib两个目录下文件的变动,如果发生变更 tomcat 将会自动重启该应用。

分析Tomcat自动加载的实现

自动加载的实现,先从Tomcat在启动之后会有一个后台线程,

ContainerBackgroundProcessor[StandardEngine[Catalina]]

定时【默认10秒】执行Engine、Host、Context、Wrapper 各容器组件及与它们相关的其它组件的 backgroundProcess 方法。- 这里开始分析。

这个方法被定义在,所有容器组件的父类org.apache.catalina.core.ContainerBase类的 backgroundProcess`方法中:

/**
*执行定期任务,如重新加载等。此方法将
*在该容器的类加载上下文中调用。意外的
*丢弃物将被捕获并记录。*/
@Override
public void backgroundProcess() {if (!getState().isAvailable())return;Cluster cluster = getClusterInternal();if (cluster != null) {try {cluster.backgroundProcess();} catch (Exception e) {log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);}}//删减后的↓↓↓↓↓↓↓ 逐个调用内部相关的backgroundProcess()方法Loader loader = getLoaderInternal();loader.backgroundProcess();//**********现在要看看的是上面↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑这个的地方了*******Manager manager = getManagerInternal();manager.backgroundProcess();Realm realm = getRealmInternal();realm.backgroundProcess();//调用管道内左右阀的backgroundProcess()方法Valve current = pipeline.getFirst();while (current != null) {current.backgroundProcess();current = current.getNext();}//最后这里注册了一个Lifecycle.PERIODIC_EVENT事件  之前分析加载web是在这个事件的处理中fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}

这里与自动加载的代码是Loader :Loader loader = getLoaderInternal();,loader.backgroundProcess();这两段。

这里看一下这个loader 变量是什么时候初始化的:【在StandardContext的startInternal 方法中】

if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader(getParentClassLoader());webappLoader.setDelegate(getDelegate());setLoader(webappLoader);
}

这里可以看到这里这个设置的Loader 的类是WebappLoader。

然后具体的关联是在Loader的backgroundProcess()中:

//public class WebappLoader extends LifecycleMBeanBase implements Loader, PropertyChangeListener {
@Override
public void backgroundProcess() {if (reloadable && modified()) {try {Thread.currentThread().setContextClassLoader(WebappLoader.class.getClassLoader());if (container instanceof StandardContext) {((StandardContext) container).reload();}} finally {......}} else {closeJARs(false);}
}

这里可以看到,这里的条件是reloadable和modified(),这里的reloadable就是配置Context节点的reloadable属性值,而modified()这个方法是对检查文件变动的,之后会分析。

先来看一下,最终要执行的重新加载的方法:StandardContext类的reload():

public synchronized void reload() {
......// Stop accepting requests temporarily.setPaused(true);try {stop();} catch (LifecycleException e) {.......}try {start();} catch (LifecycleException e) {....... }setPaused(false);
......
}

这里的reload方法中,将执行stop方法将原有的该 web 应用停掉,再调用 start 方法启动该 Context 。

start方法,则会重新加载启动web应用。【就像之前分析的那样_(:з」∠)_】

检测文件变动分析

前面,进行reload重新启动web应用的条件为:if (reloadable && modified()) {,一个为配置值,另一个就是接下来要说的了。- modified()

//public class WebappLoader extends LifecycleMBeanBase  implements Loader, PropertyChangeListener {
public boolean modified() {return classLoader != null ? classLoader.modified() : false ;
}

这里进行判断的的实际方法是:WebappLoader 的实例变量 classLoader 的 modified 方法。

说明个Tomcat中加载器的东东,每个web应用会对一个Context节点,在JVM中就会对应一个org.apache.catalina.core.StandardContext对象,而每一个StandardContext对象内部都一个加载器实例loader实例变量。可以看到前面说明,这个loader实际上是WebappLoader对象。

而每一个 WebappLoader 对象内部关联了一个 classLoader 变量(就这这个类的定义中,可以看到该变量的类型是org.apache.catalina.loader.WebappClassLoader)。

所以,这里一个web应用会对应一个StandardContext 一个WebappLoader 一个WebappClassLoader 。

WebappLoader 的初始化

WebappLoader的初始化在StandardContext 的初始化的时候已经完成了。上文中已有了:

if (getLoader() == null) {WebappLoader webappLoader = new WebappLoader(getParentClassLoader());webappLoader.setDelegate(getDelegate());setLoader(webappLoader);
}
......if ((loader != null) && (loader instanceof Lifecycle))((Lifecycle) loader).start();

这里要的代码是先初始化了,之后执行了loader的start()方法,因为WebappLoader 本身也是继承了LifecycleBase 类,所以这里的start()方法,最终也会执行到类自定义的startInternal 方法。

WebappLoader.startInternal ()方法的源码:

//public class WebappLoader extends LifecycleMBeanBase implements Loader, PropertyChangeListener {
@Override
protected void startInternal() throws LifecycleException {......// 为JNDI协议注册流处理程序工厂 ?? 啥意思啊 ╮(╯_╰)╭// Register a stream handler factory for the JNDI protocolURLStreamHandlerFactory streamHandlerFactory =DirContextURLStreamHandlerFactory.getInstance();......URL.setURLStreamHandlerFactory(streamHandlerFactory);......}// ********基于当前存储库列表构造类加载器*********需要看的就是这一段// Construct a class loader based on our current repositories listtry {classLoader = createClassLoader();   // 开始就调用了这个创建加载器的方法classLoader.setJarOpenInterval(this.jarOpenInterval);classLoader.setResources(container.getResources());classLoader.setDelegate(this.delegate);classLoader.setSearchExternalFirst(searchExternalFirst);if (container instanceof StandardContext) {classLoader.setAntiJARLocking(((StandardContext) container).getAntiJARLocking());classLoader.setClearReferencesRmiTargets(((StandardContext) container).getClearReferencesRmiTargets());classLoader.setClearReferencesStatic(((StandardContext) container).getClearReferencesStatic());classLoader.setClearReferencesStopThreads(((StandardContext) container).getClearReferencesStopThreads());classLoader.setClearReferencesStopTimerThreads(((StandardContext) container).getClearReferencesStopTimerThreads());classLoader.setClearReferencesHttpClientKeepAliveThread(((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());classLoader.setClearReferencesObjectStreamClassCaches(((StandardContext) container).getClearReferencesObjectStreamClassCaches());}for (int i = 0; i < repositories.length; i++) {classLoader.addRepository(repositories[i]);}// Configure our repositoriessetRepositories();setClassPath();setPermissions();((Lifecycle) classLoader).start();// Binding the Webapp class loader to the directory context.....} catch (Throwable t) {......}setState(LifecycleState.STARTING);
}/*** Create associated classLoader. 创建关联的类加载器。* 这里反射实例化了一个WebappClassLoader 对象。*/private WebappClassLoaderBase createClassLoader()throws Exception {Class<?> clazz = Class.forName(loaderClass);  WebappClassLoaderBase classLoader = null;if (parentClassLoader == null) {parentClassLoader = container.getParentClassLoader();}Class<?>[] argTypes = { ClassLoader.class };Object[] args = { parentClassLoader };Constructor<?> constr = clazz.getConstructor(argTypes);classLoader = (WebappClassLoaderBase) constr.newInstance(args);return classLoader;}

这里,就分析了这个要使用的类的初始化过程了。

WebappClassLoader 的 modified 方法-检测变动的代码

可以再前边看到,判断文件变动的检测代码为modified()方法:

classLoader != null ? classLoader.modified() : false

就是这句代码,所以来看一下这个**classLoader.modified()**也就是WebappClassLoader 的:

public boolean modified() {......s// Checking for modified loaded resourcesint length = paths.length;int length2 = lastModifiedDates.length;if (length > length2)length = length2;
//****这里对比资源文件里面的文件的最后修改时间是否一致,以便判断是否变动****for (int i = 0; i < length; i++) {try {long lastModified =((ResourceAttributes) resources.getAttributes(paths[i])).getLastModified();if (lastModified != lastModifiedDates[i]) {......return (true);}} catch (NamingException e) {......return (true);}}length = jarNames.length;// Check if JARs have been added or removedif (getJarPath() != null) {try {NamingEnumeration<Binding> enumeration =resources.listBindings(getJarPath());int i = 0;while (enumeration.hasMoreElements() && (i < length)) {NameClassPair ncPair = enumeration.nextElement();String name = ncPair.getName();// Ignore non JARs present in the lib folderif (!name.endsWith(".jar"))continue;if (!name.equals(jarNames[i])) {// Missing JAR......return (true);}i++;}if (enumeration.hasMoreElements()) {while (enumeration.hasMoreElements()) {NameClassPair ncPair = enumeration.nextElement();String name = ncPair.getName();// Additional non-JAR files are allowedif (name.endsWith(".jar")) {// There was more JARslog.info("    Additional JARs have been added");return (true);}}} else if (i < jarNames.length) {// There was less JARslog.info("    Additional JARs have been added");return (true);}} catch (NamingException e) {.......}}// No classes have been modifiedreturn (false);
}

这段代码从总体上看共分成两部分,第一部分检查 web 应用中的 class 文件是否有变动,根据 class 文件的最近修改时间来比较,如果有不同则直接返回true,如果 class 文件被删除也返回true

第二部分检查 web 应用中的 jar 文件是否有变动,如果有同样返回true

这里的代码看起来,还是比较容易理解的╮(╯_╰)╭

关于当前资源信息获取

关于,检查文件变动的关键代码就是:

 long lastModified =((ResourceAttributes) resources.getAttributes(paths[i])).getLastModified();if (lastModified != lastModifiedDates[i]) {

WebappClassLoader 的实例变量resources中取出文件当前的最近修改时间,与 WebappClassLoader 原来缓存的该文件的最近修改时间做比较。

这里看一下 resources.getAttributes 方法:

这里的resources实际上的是javax.naming.directory.DirContext类,看下初始化的地方,在WebappLoader 的 startInternal 方法中:【就在上面的】

 classLoader.setResources(container.getResources()); //这里设置的,是在StandardContext初始化的时候((Lifecycle) classLoader).start();

**StandardContext 中 resources 是怎么赋值:**StandardContext 的 startInternal 方法中

// Add missing components as necessary
if (webappResources == null) {   // (1) Required by Loadertry {if ((getDocBase() != null) && (getDocBase().endsWith(".war")) &&(!(new File(getBasePath())).isDirectory()))setResources(new WARDirContext()); //我们常用的wer发布加载的是这个elsesetResources(new FileDirContext()); //默认的应用是文件发布的} catch (IllegalArgumentException e) {......ok = false;}
}
if (ok) {if (!resourcesStart()) {...... } //在这里做了初始化
}

这里会对resources进行赋值,并且初始化;看下resourcesStart()初始化的方法:

//public class StandardContext extends ContainerBase
public boolean resourcesStart() {
......try {ProxyDirContext proxyDirContext =new ProxyDirContext(env, webappResources);......// 中间的太多不知道啥的东西 (ノ`Д)ノ super.setResources(proxyDirContext);   //要看的就只是这个} catch (Throwable t) {......}return (ok);
}

很明显,这里的resources 赋的是 proxyDirContext 对象,而 proxyDirContext 是一个代理对象,代理的就是 webappResources ,按上面的描述即org.apache.naming.resources.FileDirContext

org.apache.naming.resources.FileDirContext继承自抽象父类org.apache.naming.resources.BaseDirContext,而 BaseDirContext 又实现了javax.naming.directory.DirContext接口。所以 JNDI 操作中的 lookup、bind、getAttributes、rebind、search 等方法都已经在这两个类中实现了。当然里面还有 JNDI 规范之外的方法如 list 等。

所以,接下来看看一下这个getAttributes 方法的调用

最终都会调用到抽象方法 doGetAttributes 的。

//public abstract class BaseDirContext implements DirContext {
public final Attributes getAttributes(String name, String[] attrIds)throws NamingException {
......// Next do a standard lookupAttributes attrs = doGetAttributes(name, attrIds);

看一下FileDirContext 的doGetAttributes定义:

protected Attributes doGetAttributes(String name, String[] attrIds)throws NamingException {// Building attribute listFile file = file(name, true);if (file == null)         return null;return new FileResourceAttributes(file);
}

到这里就可以了,最终是调用了File的东西【java文件操作】。

实际就是根据传入的文件名查找目录下是否存在该文件,如果存在则返回包装了的文件属性对象 FileResourceAttributes 。 FileResourceAttributes 类实际是对java.io.File类做了一层包装。

关于已加载类的资源信息

还有两个内置变量pathslastModifiedDates值究竟什么时候赋的呢?

说一下 WebappClassLoader 这个自定义类加载器的用法,在 Tomcat 中所有 web 应用内WEB-INF\classes目录下的 class 文件都是用这个类加载器来加载的,一般的自定义加载器都是覆写 ClassLoader 的 findClass 方法,这里也不例外。WebappClassLoader 覆盖的是 URLClassLoader 类的 findClass 方法,而在这个方法内部最终会调用findResourceInternal(String name, String path)方法:

// Register the full path for modification checking
// Note: Only syncing on a 'constant' object is needed
synchronized (allPermission) {int j;long[] result2 =new long[lastModifiedDates.length + 1];for (j = 0; j < lastModifiedDates.length; j++) {result2[j] = lastModifiedDates[j];}result2[lastModifiedDates.length] = entry.lastModified;lastModifiedDates = result2;String[] result = new String[paths.length + 1];for (j = 0; j < paths.length; j++) {result[j] = paths[j];}result[paths.length] = fullPath;paths = result;}

这里可以看到在**加载一个新的 class 文件时会给 WebappClassLoader 的实例变量lastModifiedDatespaths数组添加元素。**这里就解答了上面提到的文件变更比较代码的疑问。要说明的是在 tomcat 启动后 web 应用中所有的 class 文件并不是全部加载的,而是配置在 web.xml 中描述的需要与应用一起加载的才会立即加载,否则只有到该类首次使用时才会由类加载器加载。

而关于 jar 包文件变动的比较代码同 class 文件比较的类似,同样是取出当前 web 应用WEB-INF\lib目录下的所有 jar 文件,与 WebappClassLoader 内部缓存的jarNames数组做比较,如果文件名不同或新加或删除了 jar 文件都返回true

这里 jarNames 变量的初始赋值代码在 WebappClassLoader 类的 addJar 方法中的开头部分…

最后这一点点,看不下去了 (╯‵□′)╯︵┻━┻

结束

2019-05-16 小杭


参考资料

  • 源码分析三:web 应用加载原理

    • 《Tomcat 7 中 web 应用加载原理(一)Context 构建》
    • 《Tomcat 7 中 web 应用加载原理(二)web.xml 解析》
    • 《Tomcat 7 中 web 应用加载原理(三)Listener、Filter、Servlet 的加载和调用》
    • 《Tomcat 7 自动加载类及检测文件变动原理》

这篇关于Tomcat 源码分析(三)-(三)-自动加载类及检测文件变动原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/879510

相关文章

MyBatis Plus实现时间字段自动填充的完整方案

《MyBatisPlus实现时间字段自动填充的完整方案》在日常开发中,我们经常需要记录数据的创建时间和更新时间,传统的做法是在每次插入或更新操作时手动设置这些时间字段,这种方式不仅繁琐,还容易遗漏,... 目录前言解决目标技术栈实现步骤1. 实体类注解配置2. 创建元数据处理器3. 服务层代码优化填充机制详

ShardingProxy读写分离之原理、配置与实践过程

《ShardingProxy读写分离之原理、配置与实践过程》ShardingProxy是ApacheShardingSphere的数据库中间件,通过三层架构实现读写分离,解决高并发场景下数据库性能瓶... 目录一、ShardingProxy技术定位与读写分离核心价值1.1 技术定位1.2 读写分离核心价值二

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深入浅出Spring中的@Autowired自动注入的工作原理及实践应用

《深入浅出Spring中的@Autowired自动注入的工作原理及实践应用》在Spring框架的学习旅程中,@Autowired无疑是一个高频出现却又让初学者头疼的注解,它看似简单,却蕴含着Sprin... 目录深入浅出Spring中的@Autowired:自动注入的奥秘什么是依赖注入?@Autowired

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

基于Redis自动过期的流处理暂停机制

《基于Redis自动过期的流处理暂停机制》基于Redis自动过期的流处理暂停机制是一种高效、可靠且易于实现的解决方案,防止延时过大的数据影响实时处理自动恢复处理,以避免积压的数据影响实时性,下面就来详... 目录核心思路代码实现1. 初始化Redis连接和键前缀2. 接收数据时检查暂停状态3. 检测到延时过

Python中的filter() 函数的工作原理及应用技巧

《Python中的filter()函数的工作原理及应用技巧》Python的filter()函数用于筛选序列元素,返回迭代器,适合函数式编程,相比列表推导式,内存更优,尤其适用于大数据集,结合lamb... 目录前言一、基本概念基本语法二、使用方式1. 使用 lambda 函数2. 使用普通函数3. 使用 N

Python脚本轻松实现检测麦克风功能

《Python脚本轻松实现检测麦克风功能》在进行音频处理或开发需要使用麦克风的应用程序时,确保麦克风功能正常是非常重要的,本文将介绍一个简单的Python脚本,能够帮助我们检测本地麦克风的功能,需要的... 目录轻松检测麦克风功能脚本介绍一、python环境准备二、代码解析三、使用方法四、知识扩展轻松检测麦

MyBatis-Plus 与 Spring Boot 集成原理实战示例

《MyBatis-Plus与SpringBoot集成原理实战示例》MyBatis-Plus通过自动配置与核心组件集成SpringBoot实现零配置,提供分页、逻辑删除等插件化功能,增强MyBa... 目录 一、MyBATis-Plus 简介 二、集成方式(Spring Boot)1. 引入依赖 三、核心机制

redis和redission分布式锁原理及区别说明

《redis和redission分布式锁原理及区别说明》文章对比了synchronized、乐观锁、Redis分布式锁及Redission锁的原理与区别,指出在集群环境下synchronized失效,... 目录Redis和redission分布式锁原理及区别1、有的同伴想到了synchronized关键字