1.为什么写这篇文档?
使用过hibernate, spring或其他大型组件,写过50个类以上的网络应用程序(web application)的开发者应该知道,当系统中有很多类时,如果开启了Tomcat的reloadable=true,那么每当相关文件改变时,Tomcat会停止web app并释放内存,然后重新加载web app.这实在是个浩大的工程。

所以我总是在想如果能有只重载某几个类的功能,将极大的满足我这个即时调试狂。
去年我在论坛上发帖,才发现已经有一些应用服务器具有了这个功能,比如WebLogic, WebSphere, 等等。好像还有一个很酷的名字,叫开发模式。看来我还是孤陋寡闻了点。
当然很多人都是在Tomcat上开发,包括我。我很喜欢它的轻小,那些大内存和高CPU消耗的应用服务器不愧为硬件杀手,没理由不改进Tomcat :)。
2.最终实现功能
我没有时间去研究Tomcat的文件监听机制,也没时间去把他写成”开发模式”这么完整的功能,我最终实现的是,实现重载功能的测试jsp--很抱歉我还是没办法写得更完整。当然,你可以在这个基础上进行改进。
3.阅读须知
阅读本文,你应该具备以下知识
1.jvm 规范有关类加载器的章节
2.Tomcat 类加载机制
3.java 反射机制
4.ant
(好象该网址被不定时封锁,有时能上,有时不能)
最好在你的电脑上安装ant,因为Tomcat源码包使用ant从互联网获得依赖包。不过我也是修改了一个错误才使它完全编译通过。
当然,你也可以用其他IDE工具检查并添加依赖包,在IDE中,其实你只需要添加jar直到使org.apache.catalina.loader.WebappClassLoader无错即可。
4.修改过程
1.说明
新添加的代码请添加到java文件的末尾,因为我在说明行数的时候,尽量符合原始行数
2.web app类加载器
在Tomcat中,org.apache.catalina.loader.WebappClassLoader是web app的类加载器,所以需要修改它实现重载功能。
3.资源列表
在WebappClassLoader中,有一个Map类型属性resourceEntries,它记载了web app中WEB-INF/classes目录下所加载的类,因此当我们需要重载一个类时,我们需要先将它在resourceEntries里删除,我编写了一个方法方便调用:
public boolean removeResourceEntry(String name) {
     if (resourceEntries.containsKey(name)) {
         resourceEntries.remove(name);
         return true;
     }
     return false;
}
4.是否重载标志
让WebappClassLoader需要知道加载一个类是否使用重载的方式。所以我建立一个boolean 类型的属性和实现它的getter/setter方法:
private boolean isReload = false;

      public boolean isReload() {
          return isReload;
      }

      public void setReload(boolean isReload) {
          this.isReload = isReload;
      }
5.动态类加载器
根据jvm类加载器规范,一个类加载器对象只能加载一个类1次,所以重载实际上是创建出另一个类加载器对象来加载同一个类。当然,我们不需要再创建一个WebappClassLoader,他太大而且加载规则很复杂,不是我们想要的,所以我们创建一个简单的类加载器类org.apache.catalina.loader.DynamicClassLoader:
package org.apache.catalina.loader;

import java.net.URL;
import java.net.URLClassLoader;
import java.security.CodeSource;
import java.util.*;

/**
* 动态类加载器
*
* @author peter
*
*/
public class DynamicClassLoader extends URLClassLoader {
    /* 父类加载器 */
    private ClassLoader parent = null;

    /* 已加载类名列表 */
    private List classNames = null;

    /**
    * 构造器
    *
    * @param parent
    * 父类加载器,这里传入的是WebappClassLoader
    */
    public DynamicClassLoader(ClassLoader parent) {
        super(new URL[0]);
        classNames = new ArrayList();
        this.parent = parent;
    }

    /**
    * 从类的二进制数据中加载类.
    *
    * @param name
    * 类名
    * @param classData
    * 类的二进制数据
    * @param codeSource
    * 数据来源
    * @return 成功加载的类
    * @throws ClassNotFoundException
    * 加载失败抛出未找到此类异常
    */
    public Class loadClass(String name, byte[] classData, CodeSource codeSource) throws ClassNotFoundException {
        if (classNames.contains(name)) {
            // System.out.println("此类已存在,调用 loadClass 方法加载.");
            return loadClass(name);
        } else {
            // System.out.println("新类, 记录到类名列表,并用类定义方法加载类");
            classNames.add(name);
            return defineClass(name, classData, 0, classData.length, codeSource);
        }
    }

    /* *
    * 重载此方法,当要加载的类不在类名列表中时,调用父类加载器方法加载.
    * @see java.lang.ClassLoader#loadClass(java.lang.String)
    */
    public Class loadClass(String name) throws ClassNotFoundException {
        if (!classNames.contains(name)) {
            //System.out.println("不在类名列表中,调用父类加载器方法加载");
            return parent.loadClass(name);
        }
        return super.loadClass(name);
    }
}
6.在webappClassLoader中添加DynamicClassLoader
1.添加属性
private DynamicClassLoader dynamicClassLoader = new DynamicClassLoader(this);
2.添加重建方法,以便需要再次重载时替换掉上次的类加载器对象
public void reCreateDynamicClassLoader() {
                dynamicClassLoader = new DynamicClassLoader(this);
            }
7.修改调用点
1.第832行,公开findClass方法
public Class findClass(String name) throws ClassNotFoundException {
2.第1569行,添加如下一行代码。
if (isReload) removeResourceEntry(name);
3.第1577行,这里好像是一个bug,具体原因我忘了-_-||
if ((entry == null) || (entry.binaryContent == null))
改为
if ((entry == null) || (entry.loadedClass == null && entry.binaryContent == null))
4.第1633~1636行
if (entry.loadedClass == null) {
                clazz = defineClass(name, entry.binaryContent, 0, entry.binaryContent.length,
                    codeSource);
            改为
            byte[] classData = new byte[entry.binaryContent.length];
            System.arraycopy(entry.binaryContent, 0, classData, 0,
            classData.length);
            if (entry.loadedClass == null) {
                clazz = isReload ?
                    dynamicClassLoader.loadClass(name,
                    classData, codeSource) :
                    defineClass(name,
                    classData, 0, classData.length, codeSource);
8.测试代码
1.test.jsp
我测试用的jsp为$CATALINA_HOME/webapps/ROOT/test.jsp,由于webapp里面并不会显式加载tomcat的核心类,所以我们需要用反射代码调用WebappClassLoader的方法。代码如下:
<%
ClassLoader loader = (Thread.currentThread().getContextClassLoader());
Class clazz = loader.getClass();
java.lang.reflect.Method setReload = clazz.getMethod("setReload", new Class[]{boolean.class});
java.lang.reflect.Method reCreate = clazz.getMethod("reCreateDynamicClassLoader", null);
java.lang.reflect.Method findClass = clazz.getMethod("findClass", new Class[]{String.class});

reCreate.invoke(loader, null);
setReload.invoke(loader, new Object[]{true});
Class A = (Class)findClass.invoke(loader, new Object[]{"org.AClass"});
setReload.invoke(loader, new Object[]{false});
A.newInstance();
// 如果你使用下面这行代码,当重编译类时,请稍微修改一下调用它的jsp,让jsp也重新编译
//org.AClass a = (org.AClass)A.newInstance();

// 下面这些代码是测试当一个类不在DynamicClassLoader类名列表时的反应
//a.test();
//java.lang.reflect.Method test = a.getClass().getMethod("test", null);
//test.invoke(a, null);
%>
2.org.AClass
package org;

        public class AClass {
            public AClass() {
                // 修改输出内容确认Tomcat重新加载了类
                System.out.println("AClass v3");
            }

            public void createBClass() {
                new BClass();
            }
        }
3.org.BClass
package org;

        public class BClass {
            public BClass() {
                //修改输出内容确认Tomcat重新加载了类
                System.out.println("BClass v1");
            }
        }
9.测试步骤
1.按照上述步骤修改Tomcat源码并编译。
2.用winzip/winrar/file-roller打开$CATALINA_HOME/server/lib/catalina.jar。把前面编译完成后的org.apache.catalina.loader目录下的class文件覆盖jar中同名文件。
3.编译org.AClass和org.BClass
4.启动Tomcat并在浏览器中打开测试页 http://localhost:8080/test.jsp
5.修改org.AClass中的System.out.println();语句并重编译类。
6.按下F5按键刷新浏览器。
7.查看Tomcat控制台是否输出了不同的语句?
8.Good Luck! :)))

了解更多请移步路人甲技术交流 http://www.walkerjava.com 期待您的加入