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

相关文章

Springboot中分析SQL性能的两种方式详解

《Springboot中分析SQL性能的两种方式详解》文章介绍了SQL性能分析的两种方式:MyBatis-Plus性能分析插件和p6spy框架,MyBatis-Plus插件配置简单,适用于开发和测试环... 目录SQL性能分析的两种方式:功能介绍实现方式:实现步骤:SQL性能分析的两种方式:功能介绍记录

Python如何实现PDF隐私信息检测

《Python如何实现PDF隐私信息检测》随着越来越多的个人信息以电子形式存储和传输,确保这些信息的安全至关重要,本文将介绍如何使用Python检测PDF文件中的隐私信息,需要的可以参考下... 目录项目背景技术栈代码解析功能说明运行结php果在当今,数据隐私保护变得尤为重要。随着越来越多的个人信息以电子形

最长公共子序列问题的深度分析与Java实现方式

《最长公共子序列问题的深度分析与Java实现方式》本文详细介绍了最长公共子序列(LCS)问题,包括其概念、暴力解法、动态规划解法,并提供了Java代码实现,暴力解法虽然简单,但在大数据处理中效率较低,... 目录最长公共子序列问题概述问题理解与示例分析暴力解法思路与示例代码动态规划解法DP 表的构建与意义动

关于Spring @Bean 相同加载顺序不同结果不同的问题记录

《关于Spring@Bean相同加载顺序不同结果不同的问题记录》本文主要探讨了在Spring5.1.3.RELEASE版本下,当有两个全注解类定义相同类型的Bean时,由于加载顺序不同,最终生成的... 目录问题说明测试输出1测试输出2@Bean注解的BeanDefiChina编程nition加入时机总结问题说明

Go Mongox轻松实现MongoDB的时间字段自动填充

《GoMongox轻松实现MongoDB的时间字段自动填充》这篇文章主要为大家详细介绍了Go语言如何使用mongox库,在插入和更新数据时自动填充时间字段,从而提升开发效率并减少重复代码,需要的可以... 目录前言时间字段填充规则Mongox 的安装使用 Mongox 进行插入操作使用 Mongox 进行更

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

Tomcat高效部署与性能优化方式

《Tomcat高效部署与性能优化方式》本文介绍了如何高效部署Tomcat并进行性能优化,以确保Web应用的稳定运行和高效响应,高效部署包括环境准备、安装Tomcat、配置Tomcat、部署应用和启动T... 目录Tomcat高效部署与性能优化一、引言二、Tomcat高效部署三、Tomcat性能优化总结Tom

通过prometheus监控Tomcat运行状态的操作流程

《通过prometheus监控Tomcat运行状态的操作流程》文章介绍了如何安装和配置Tomcat,并使用Prometheus和TomcatExporter来监控Tomcat的运行状态,文章详细讲解了... 目录Tomcat安装配置以及prometheus监控Tomcat一. 安装并配置tomcat1、安装

MySQL中的MVCC底层原理解读

《MySQL中的MVCC底层原理解读》本文详细介绍了MySQL中的多版本并发控制(MVCC)机制,包括版本链、ReadView以及在不同事务隔离级别下MVCC的工作原理,通过一个具体的示例演示了在可重... 目录简介ReadView版本链演示过程总结简介MVCC(Multi-Version Concurr

C#使用DeepSeek API实现自然语言处理,文本分类和情感分析

《C#使用DeepSeekAPI实现自然语言处理,文本分类和情感分析》在C#中使用DeepSeekAPI可以实现多种功能,例如自然语言处理、文本分类、情感分析等,本文主要为大家介绍了具体实现步骤,... 目录准备工作文本生成文本分类问答系统代码生成翻译功能文本摘要文本校对图像描述生成总结在C#中使用Deep