在文章《Wicket1.3中Class热加载--使用篇》中,展示了如何使用Wicket1.3提供的ReloadingWicketFilter来动态加载修改后的类(包括修改了签名的类),从而实现高效开发。但在该文章中,只是给出了如何使用该功能的说明,而本篇文章将明确解析Wicket1.3类热加载魔法的奥秘所在。
在上一篇文章中,是通过修改Wicket项目中web.xml文件,将其中的
org.apache.wicket.protocol.http.WicketFilter
全部替换成
org.apache.wicket.protocol.http.ReloadingWicketFilter
从而开启了Wicket类热加载的功能。那么为了探究其魔法奥秘,入手点自然就选择ReloadingWicketFilter这个类了。
先来看一下ReloadingWicketFilter的代码,惊人的少:
    
        
            | 
             public class ReloadingWicketFilter extends WicketFilter 
            { 
            private ReloadingClassLoader reloadingClassLoader; 
             
            /** 
             * Instantiate the reloading class loader 
             */ 
            public ReloadingWicketFilter() 
            { 
            // Create a reloading classloader 
            reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader());  
            } 
             
            /** 
             * @see org.apache.wicket.protocol.http.WicketFilter#getClassLoader() 
             */ 
            protected ClassLoader getClassLoader() 
            { 
            return reloadingClassLoader; 
            } 
             
            /** 
             * @see org.apache.wicket.protocol.http.WicketFilter#init(javax.servlet.FilterConfig) 
             */ 
            public void init(final FilterConfig filterConfig) throws ServletException 
            { 
            reloadingClassLoader.setListener(new IChangeListener() 
            { 
            public void onChange() 
            { 
            // Remove the ModificationWatcher from the current reloading class loader 
            reloadingClassLoader.destroy(); 
             
            /* 
             * Create a new classloader, as there is no way to clear a ClassLoader's cache. This 
             * supposes that we don't share objects across application instances, this is almost 
             * true, except for Wicket's Session object. 
             */ 
            reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader()); 
            try 
            { 
            init(filterConfig); 
            } 
            catch (ServletException e) 
            { 
            Throw new RuntimeException(e); 
            } 
            } 
            }); 
             
            super.init(filterConfig); 
            } 
            } 
             
             | 
        
    
其中最引人注目的就是那个ReloadingClassLoader,再打开WicketFilter类中,很容易找到以下代码,它表示使用自定义的ClassLoader来加载类。
    
        
            | 
             final ClassLoader newClassLoader = getClassLoader(); 
            Thread.currentThread().setContextClassLoader(newClassLoader); 
             | 
        
    
而ReloadingWicketFilter则是重载了getClassLoader方法,以返回了自定义的ReloadingClassLoader。也就是说Wicket的魔法其实是使用了一个自定义的ReloadingClassLoader来实现类的热加载(包括对签名被修改的类)。为了让大家更清楚理解Wicket这一方法以,在分析ReloadingClassLoader之前,先来简单的过一下JVM的类加载机制。
在JDK1.2以后,JVM在加载类时默认采用的是双亲委托机制(早期的类加载机制存在安全漏洞)。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,所有 ClassLoaders 的根都是系统 ClassLoader,它会以缺省方式装入类,即从本地文件系统加载(可能是Jar包,也可能是Class文件),如果父类加载器可以完成类加载任务,就成功返回加载后的类;但如果父类加载器无法完成此加载任务时,那么这个特定的类加载才自己去加载指定名称的类。这样的双亲委托机制可以保证象java.io.*这种基础类库的内容一定是被系统ClassLoader加载,从而保证类加载的安全性。
下面是双亲委派机制的示意图:

实例分析Web应用下的类加载顺序:
为了更好的方便大家理解类的加载机制,并说明Wicket如何使用自定义的ClassLoader来加载更改后的类,下面将有一个简单的实例来说明。
首先将前一篇文章中的HelloWorld代码修改为:
    
        
            | 
             public class HelloWorld extends WicketExamplePage 
            { 
            /** 
             * Constructor 
             */ 
            public HelloWorld() 
            { 
            ClassLoader classLoader = this.getClass().getClassLoader(); 
             
            while (null != classLoader)  
            { 
            System.err.println("loader   " + classLoader.hashCode()+"   "+classLoader.getClass());  
            classLoader = classLoader.getParent(); 
            } 
             
            add(new Label("message", "New Hello World!")); 
            } 
            } 
             | 
        
    
修改以后的代码,可以在对象被创建时,输出HelloWorld类的ClassLoader及其父ClassLoader,接下来恢复web.xml文件中的filter为WicketFilter,不使用RelodingWicketFilter,从而观察原先的Class加载顺序,得到的结果为:
    
        
            | 
             loader   2011334   class org.apache.catalina.loader.WebappClassLoader 
            loader   19608393   class org.apache.catalina.loader.StandardClassLoader 
            loader   13756574   class org.apache.catalina.loader.StandardClassLoader 
            loader   26726999   class sun.misc.Launcher$AppClassLoader 
            loader   7494106   class sun.misc.Launcher$ExtClassLoader 
             | 
        
    
接下来仍然按照上一篇文章中的操作修改web.xml,使用RelodingWicketFilter,开启Wicket类的热加载功能。再看一下输出结果:
    
        
            | 
             loader   8310913   class org.apache.wicket.application.ReloadingClassLoader 
            loader   2011334   class org.apache.catalina.loader.WebappClassLoader 
            loader   19608393   class org.apache.catalina.loader.StandardClassLoader 
            loader   13756574   class org.apache.catalina.loader.StandardClassLoader 
            loader   26726999   class sun.misc.Launcher$AppClassLoader 
            loader   7494106   class sun.misc.Launcher$ExtClassLoader 
             | 
        
    
可见通过代码
    
        
            | 
             final ClassLoader newClassLoader = getClassLoader(); 
            Thread.currentThread().setContextClassLoader(newClassLoader); 
             | 
        
    
ReloadingWicketFilter使用ReloadingClassLoader作为当前类的ClassLoader,也就是说它接管了所有WEB-INF/classes目录下面类文件的加载。这样它就可以根据实际情况来加载一个类。但有经验的程序员都有知道,一般来说Class一旦被加载,就表示在整个JVM生命周期的过程中,不会自动释放,而是放置在内存中。那么Wicket又是怎么释放已经加载的类,同时加载修改后的类呢?看一段ReloadingWicketFilter中init方法的代码:
    
        
            | 
             /** 
             * @see org.apache.wicket.protocol.http.WicketFilter#init(javax.servlet.FilterConfig) 
             */ 
            public void init(final FilterConfig filterConfig) throws ServletException 
            { 
            reloadingClassLoader.setListener(new IChangeListener() 
            { 
            public void onChange() 
            { 
            // Remove the ModificationWatcher from the current reloading class loader 
            reloadingClassLoader.destroy(); 
             
            /* 
             * Create a new classloader, as there is no way to clear a ClassLoader's cache. This 
             * supposes that we don't share objects across application instances, this is almost 
             * true, except for Wicket's Session object. 
             */ 
            reloadingClassLoader = new ReloadingClassLoader(getClass().getClassLoader()); 
            try 
            { 
            init(filterConfig); 
            } 
            catch (ServletException e) 
            { 
            Throw new RuntimeException(e); 
            } 
            } 
            }); 
             
            super.init(filterConfig); 
            } 
             
             | 
        
    
这段代码表示,一旦发现类文件的改变,将会销毁当前的reloadingClassLoader ,同时新建一个ReloadingClassLoader的实例,因为ClassLoader被销毁了,所以由该ClassLoader加载的类都将被销毁,然后再由新的ReloadingClassLoader进行加载,因此即使是修改了签名的类也是可以被正确加载成功的。当然这种做法,可能会引起Session中的数据不能正确识别和转换。但相对于开发环境下,开发人员通过另起一个新的Session就可以开始正常的工作了,还是可以有效的提高开发效率。
Wicket类加载的一个潜在问题:
如果仅仅使用Wicket开发程序,那么Wicket1.3引入的热加载机制,对开发人员来说,会是一件非常幸福的事情,但如果同时使用了jsp,也就是说同时在一个Web应用程序(不是Web应用服务器)中同时使用Wicket和JSP,而且在Wicket和JSP代码分别向Session中写入相同的对象,如用户信息之类的数据对象,那么就会出现一些不必要的问题,最觉的莫过于ClassCastException。虽然共用Wicket+JSP的情况比较少,出问题的机率也比较少,但还是要额外提出来作为一个警示。
下面是一个简单的类Person代码,用来展示如何出现ClassCastException:
    
        
            | 
             public class Person  
            { 
             
            /** 
             * Default constructor 
             */ 
            public Person()  
            { 
            super(); 
             
            ClassLoader classLoader = this.getClass().getClassLoader(); 
             
            while (null != classLoader)  
            { 
            System.err.println("loader   " + classLoader.hashCode()+"   "+classLoader.getClass());  
            classLoader = classLoader.getParent(); 
            } 
            } 
            } 
             | 
        
    
在构造函数中的那段代码,可以输出它的ClassLoader顺序,接下来象先前一样访问那个HelloWorld应用,得到如下的ClassLoader顺序:
    
        
            | 
             loader   8310913   class org.apache.wicket.application.ReloadingClassLoader 
            loader   3862294   class org.apache.catalina.loader.WebappClassLoader 
            loader   19608393   class org.apache.catalina.loader.StandardClassLoader 
            loader   13756574   class org.apache.catalina.loader.StandardClassLoader 
            loader   26726999   class sun.misc.Launcher$AppClassLoader 
            loader   7494106   class sun.misc.Launcher$ExtClassLoader 
             | 
        
    
然后再写一个run.jsp,代码如下:
    
        
            | 
             <%@page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> 
            <%@page import="org.apache.wicket.examples.Person"%> 
            <html> 
            <head> 
            <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> 
            <title>Class Loader Demo</title> 
            </head> 
            <body> 
            <% 
            new Person(); 
            %> 
            </body> 
            </html> 
             | 
        
    
在run.jsp中,初始化一个Person对象,同样观察它的输出结果,得到另外一个不同的Class加载顺序:
    
        
            | 
             loader   3862294   class org.apache.catalina.loader.WebappClassLoader 
            loader   19608393   class org.apache.catalina.loader.StandardClassLoader 
            loader   13756574   class org.apache.catalina.loader.StandardClassLoader 
            loader   26726999   class sun.misc.Launcher$AppClassLoader 
            loader   7494106   class sun.misc.Launcher$ExtClassLoader 
             | 
        
    
可见在JSP中的Person与在HelloWorld中的Person是由不同的ClassLoader加载的(JSP编译成Servlet执行,在Tomcat中,org.apache.jasper.servlet.JasperLoader负责JSP编译后的类加载),根据JVM规范,这两个Class是不等价的。因此进行转换的时候,会引起ClassCastException,这是特别需要注意的一点。
点击这里下载Word格式