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

相关文章

Spring Boot 配置文件之类型、加载顺序与最佳实践记录

《SpringBoot配置文件之类型、加载顺序与最佳实践记录》SpringBoot的配置文件是灵活且强大的工具,通过合理的配置管理,可以让应用开发和部署更加高效,无论是简单的属性配置,还是复杂... 目录Spring Boot 配置文件详解一、Spring Boot 配置文件类型1.1 applicatio

Tomcat版本与Java版本的关系及说明

《Tomcat版本与Java版本的关系及说明》:本文主要介绍Tomcat版本与Java版本的关系及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Tomcat版本与Java版本的关系Tomcat历史版本对应的Java版本Tomcat支持哪些版本的pythonJ

Python中随机休眠技术原理与应用详解

《Python中随机休眠技术原理与应用详解》在编程中,让程序暂停执行特定时间是常见需求,当需要引入不确定性时,随机休眠就成为关键技巧,下面我们就来看看Python中随机休眠技术的具体实现与应用吧... 目录引言一、实现原理与基础方法1.1 核心函数解析1.2 基础实现模板1.3 整数版实现二、典型应用场景2

Python实现无痛修改第三方库源码的方法详解

《Python实现无痛修改第三方库源码的方法详解》很多时候,我们下载的第三方库是不会有需求不满足的情况,但也有极少的情况,第三方库没有兼顾到需求,本文将介绍几个修改源码的操作,大家可以根据需求进行选择... 目录需求不符合模拟示例 1. 修改源文件2. 继承修改3. 猴子补丁4. 追踪局部变量需求不符合很

Java的IO模型、Netty原理解析

《Java的IO模型、Netty原理解析》Java的I/O是以流的方式进行数据输入输出的,Java的类库涉及很多领域的IO内容:标准的输入输出,文件的操作、网络上的数据传输流、字符串流、对象流等,这篇... 目录1.什么是IO2.同步与异步、阻塞与非阻塞3.三种IO模型BIO(blocking I/O)NI

SpringBoot中封装Cors自动配置方式

《SpringBoot中封装Cors自动配置方式》:本文主要介绍SpringBoot中封装Cors自动配置方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录SpringBoot封装Cors自动配置背景实现步骤1. 创建 GlobalCorsProperties

Spring事务中@Transactional注解不生效的原因分析与解决

《Spring事务中@Transactional注解不生效的原因分析与解决》在Spring框架中,@Transactional注解是管理数据库事务的核心方式,本文将深入分析事务自调用的底层原理,解释为... 目录1. 引言2. 事务自调用问题重现2.1 示例代码2.2 问题现象3. 为什么事务自调用会失效3

idea中创建新类时自动添加注释的实现

《idea中创建新类时自动添加注释的实现》在每次使用idea创建一个新类时,过了一段时间发现看不懂这个类是用来干嘛的,为了解决这个问题,我们可以设置在创建一个新类时自动添加注释,帮助我们理解这个类的用... 目录前言:详细操作:步骤一:点击上方的 文件(File),点击&nbmyHIgsp;设置(Setti

tomcat多实例部署的项目实践

《tomcat多实例部署的项目实践》Tomcat多实例是指在一台设备上运行多个Tomcat服务,这些Tomcat相互独立,本文主要介绍了tomcat多实例部署的项目实践,具有一定的参考价值,感兴趣的可... 目录1.创建项目目录,测试文China编程件2js.创建实例的安装目录3.准备实例的配置文件4.编辑实例的

如何将Tomcat容器替换为Jetty容器

《如何将Tomcat容器替换为Jetty容器》:本文主要介绍如何将Tomcat容器替换为Jetty容器问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Tomcat容器替换为Jetty容器修改Maven依赖配置文件调整(可选)重新构建和运行总结Tomcat容器替