专栏名称: 预流
敲代码的
目录
相关文章推荐
研之成理  ·  北京大学郭少军教授团队,Nature ... ·  昨天  
募格学术  ·  年仅43岁!西南政法大学副教授去世 ·  昨天  
PaperWeekly  ·  NeurIPS 2024 | ... ·  3 天前  
51好读  ›  专栏  ›  预流

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

预流  · 掘金  ·  · 2018-02-28 03:56

正文

在一般的 web 应用开发里通常会使用开发工具(如 Eclipse、IntelJ )集成 tomcat ,这样可以将 web 工程项目直接发布到 tomcat 中,然后一键启动。经常遇到的一种情况是直接修改一个类的源文件,此时开发工具会直接将编译后的 class 文件发布到 tomcat 的 web 工程里,但如果 tomcat 没有配置应用的自动加载功能的话,当前 JVM 中运行的 class 还是源文件修改之前编译好的 class 文件。可以重启 tomcat 来加载新的 class 文件,但这样做需要再手工点击一次 restart ,为了能够在应用中即时看到 java 文件修改之后的执行情况,可以在 tomcat 中将应用配置成自动加载模式,其配置很简单,只要在配置文件的 Context 节点中加上一个 reloadable 属性为 true 即可,示例如下:

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

如果你的开发工具已经集成了 tomcat 的话应该会有一个操作界面配置来代替手工添加文件信息,如 Eclipse 中是如下界面来配置的:

此时需要把 Auto reloading enabled 前面的复选框钩上。其背后的原理实际也是在 server.xml 文件中加上 Context 节点的描述:

<Context docBase="test" path="/test" reloadable="true"/>

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

熟悉 Tomcat 的人应该都用过这个功能,就不再详述它的配置步骤了。我感兴趣的是这个自动加载功能在 Tomcat 7 中是怎么实现的。

前面的文章 中曾经讲过 Tomcat 7 在启动完成后会有一个后台线程 ContainerBackgroundProcessor[StandardEngine[Catalina]] ,这个线程将会定时(默认为 10 秒)执行 Engine、Host、Context、Wrapper 各容器组件及与它们相关的其它组件的 backgroundProcess 方法,这段代码在所有容器组件的父类 org.apache.catalina.core.ContainerBase 类的 backgroundProcess`方法中:


public void backgroundProcess() {
    
    if (!getState().isAvailable())
        return;

    if (cluster != null) {
        try {
            cluster.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.cluster", cluster), e);                
        }
    }
    if (loader != null) {
        try {
            loader.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.loader", loader), e);                
        }
    }
    if (manager != null) {
        try {
            manager.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.manager", manager), e);                
        }
    }
    Realm realm = getRealmInternal();
    if (realm != null) {
        try {
            realm.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.realm", realm), e);                
        }
    }
    Valve current = pipeline.getFirst();
    while (current != null) {
        try {
            current.backgroundProcess();
        } catch (Exception e) {
            log.warn(sm.getString("containerBase.backgroundProcess.valve", current), e);                
        }
        current = current.getNext();
    }
    fireLifecycleEvent(Lifecycle.PERIODIC_EVENT, null);
}

与自动加载类相关的代码在 loader 的 backgroundProcess 方法的调用时。每一个 StandardContext 会关联一个 loader 变量,该变量的初始化在 org.apache.catalina.core.StandardContext 类的 startInternal 方法中的这段代码:


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

所以上面的 loader.backgroundProcess() 方法的调用将会执行 org.apache.catalina.loader.WebappLoader 类的 backgroundProcess 方法:


public void backgroundProcess() {
    if (reloadable && modified()) {
        try {
            Thread.currentThread().setContextClassLoader
                (WebappLoader.class.getClassLoader());
            if (container instanceof StandardContext) {
                ((StandardContext) container).reload();
            }
        } finally {
            if (container.getLoader() != null) {
                Thread.currentThread().setContextClassLoader
                    (container.getLoader().getClassLoader());
            }
        }
    } else {
        closeJARs(false);
    }
}

其中 reloadable 变量的值就是本文开始提到的配置文件的 Context 节点的 reloadable 属性的值,当它为 true 并且 modified() 方法返回也是 true 时就会执行 StandardContext 的 reload 方法:


public synchronized void reload() {

    // Validate our current component state
    if (!getState().isAvailable())
        throw new IllegalStateException
            (sm.getString("standardContext.notStarted", getName()));

    if(log.isInfoEnabled())
        log.info(sm.getString("standardContext.reloadingStarted",
                getName()));

    // Stop accepting requests temporarily.
    setPaused(true);

    try {
        stop();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.stoppingContext", getName()), e);
    }

    try {
        start();
    } catch (LifecycleException e) {
        log.error(
            sm.getString("standardContext.startingContext", getName()), e);
    }

    setPaused(false);

    if(log.isInfoEnabled())
        log.info(sm.getString("standardContext.reloadingCompleted",
                getName()));

}

reload 方法中将先执行 stop 方法将原有的该 web 应用停掉,再调用 start 方法启动该 Context ,start 方法的分析前文已经说过,stop 方法可以参照 start 方法一样分析,不再赘述。

这里重点要说的是上面提到的监控文件变动的方法 modified ,只有它返回 true 才会导致应用自动加载。看下该方法的实现:

public boolean modified() {
    return classLoader != null ? classLoader.modified() : false ;
}

可以看到这里面实际调用的是 WebappLoader 的实例变量 classLoader 的 modified 方法来判断的,下文就详细分析这个 modified 方法的实现。

先简要说一下 Tomcat 中的加载器。在 Tomcat 7 中每一个 web 应用对应一个 Context 节点,这个节点在 JVM 中就对应一个 org.apache.catalina.core.StandardContext 对象,而每一个 StandardContext 对象内部都有一个加载器实例变量(即其父类 org.apache.catalina.core.ContainerBase loader 实例变量),前面已经看到这个 loader 变量实际上是 org.apache.catalina.loader.WebappLoader 对象。而每一个 WebappLoader 对象内部关联了一个 classLoader 变量(就这这个类的定义中,可以看到该变量的类型是 org.apache.catalina.loader.WebappClassLoader )。

在 Tomcat 7 的源码中给出了 6 个 web 应用:

所以在 Tomcat 启动完成之后理论上应该有 6 个 StandardContext 对象,6 个 WebappLoader 对象,6 个 WebappClassLoader 对象。用 jvisualvm 观察实际情况也证实了上面的判断:

StandardContext 实例数:

StandardContext 实例数

WebappLoader 实例数:

WebappLoader 实例数

WebappClassLoader 实例数

WebappClassLoader 实例数

上面讲过了 WebappLoader 的初始化代码,接下来讲一下 WebappClassLoader 的对象初始化代码。同样还是在 StandardContext 类的 startInternal 方法中,有如下两段代码:

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

这一段是上面已经说过的 WebappLoader 的初始化。

try {

    if (ok) {
        
        // Start our subordinate components, if any
        if ((loader != null) && (loader instanceof Lifecycle))
            ((Lifecycle) loader).start();

这一段与 WebappLoader 的对象相关,执行的就是 WebappLoader 类的 start 方法,因为 WebappLoader 继承自 LifecycleBase 类,所以调用它的 start 方法最终将会执行该类自定义的 startInternal 方法,看下 startInternal 方法中的这段代码:


classLoader = createClassLoader();
classLoader.setResources(container.getResources());
classLoader.setDelegate(this.delegate);
classLoader.setSearchExternalFirst(searchExternalFirst);
if (container instanceof StandardContext) {
    classLoader.setAntiJARLocking(
            ((StandardContext) container).getAntiJARLocking());
    classLoader.setClearReferencesStatic(
            ((StandardContext) container).getClearReferencesStatic());
    classLoader.setClearReferencesStopThreads(
            ((StandardContext) container).getClearReferencesStopThreads());
    classLoader.setClearReferencesStopTimerThreads(
            ((StandardContext) container).getClearReferencesStopTimerThreads());
    classLoader.setClearReferencesHttpClientKeepAliveThread(
            ((StandardContext) container).getClearReferencesHttpClientKeepAliveThread());
}

for (int i = 0; i < repositories.length; i++) {
    classLoader.addRepository(repositories[i]);
}

// Configure our repositories
setRepositories();
setClassPath();

setPermissions();

((Lifecycle) classLoader).start();

一开始调用了 createClassLoader 方法:


/**
 * Create associated classLoader.
 */
private WebappClassLoader createClassLoader()
    throws Exception {

    Class clazz = Class.forName(loaderClass);
    WebappClassLoader classLoader = null;

    if (parentClassLoader == null) {
        parentClassLoader = container.getParentClassLoader();
    }
    Class[] argTypes = { ClassLoader.class };
    Object[] args = { parentClassLoader };
    Constructor constr = clazz.getConstructor(argTypes);
    classLoader = (WebappClassLoader) constr.newInstance(args);

    return classLoader;

}

可以看出这里通过反射实例化了一个 WebappClassLoader 对象。

回到文中上面提的问题,看下 WebappClassLoader 的 modified 方法代码:


/**
 * Have one or more classes or resources been modified so that a reload
 * is appropriate?
 */
public boolean modified() {

    if (log.isDebugEnabled())
        log.debug("modified()");

    // Checking for modified loaded resources
    int length = paths.length;

    // A rare race condition can occur in the updates of the two arrays
    // It's totally ok if the latest class added is not checked (it will
    // be checked the next time
    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]) {
                if( log.isDebugEnabled() )
                    log.debug("  Resource '" + paths[i]
                              + "' was modified; Date is now: "
                              + new java.util.Date(lastModified) + " Was: "
                              + new java.util.Date(lastModifiedDates[i]));
                return (true);
            }
        } catch (NamingException e) {
            log.error("    Resource '" + paths[i] + "' is missing");
            return (true);
        }
    }

    length = jarNames.length;

    // Check if JARs have been added or removed
    if (getJarPath() != null) {

        try {
            NamingEnumeration 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 folder
                if (!name.endsWith(".jar"))
                    continue;
                if (!name.equals(jarNames[i])) {
                    // Missing JAR
                    log.info("    Additional JARs have been added : '"
                             + name + "'");
                    return (true);
                }
                i++;
            }
            if (enumeration.hasMoreElements()) {
                while (enumeration.hasMoreElements()) {
                    NameClassPair ncPair = enumeration.nextElement();
                    String name = ncPair.getName();
                    // Additional non-JAR files are allowed
                    if (name.endsWith(".jar")) {
                        // There was more JARs
                        log.info("    Additional JARs have been added");
                        return (true);
                    }
                }
            } else if (i < jarNames.length) {
                // There was less JARs
                log.info("    Additional JARs have been added");
                return (true);
            }
        } catch (NamingException e) {
            if (log.isDebugEnabled())
                log.debug("    Failed tracking modifications of '"
                    + getJarPath() + "'");
        } catch (ClassCastException e) {
            log.error("    Failed tracking modifications of '"
                      + getJarPath() + "' : " + e.getMessage());
        }

    }

    // No classes have been modified
    return (false);

}

这段代码从总体上看共分成两部分,第一部分检查 web 应用中的 class 文件是否有变动,根据 class 文件的最近修改时间来比较,如果有不同则直接返回 true ,如果 class 文件被删除也返回 true 。第二部分检查 web 应用中的 jar 文件是否有变动,如果有同样返回 true 。稍有编程经验的人对于以上比较代码都容易理解,但对这些变量的值,特别是里面比较时经常用到 WebappClassLoader 类的实例变量的值是在什么地方赋值的会比较困惑,这里就这点做一下说明。







请到「今天看啥」查看全文