国内最全IT社区平台 联系我们 | 收藏本站
华晨云阿里云优惠2
您当前位置:首页 > 服务器 > Tomcat系列(一)Tomcat热部署的实现原理

Tomcat系列(一)Tomcat热部署的实现原理

来源:程序员人生   发布时间:2017-03-09 09:12:19 阅读次数:5104次

Tomcat热部署机制

对Java利用程序来讲,热部署就是在运行时更新Java类文件。在基于Java的利用服务器实现热部署的进程中,类装入器扮演侧重要的角色。大多数基于Java的利用服务器,包括EJB服务器和Servlet容器,都支持热部署。类装入器不能重新装入1个已装入的类,但只要使用1个新的类装入器实例,就能够将类再次装入1个正在运行的利用程序。

我们知道,现在大多数的web服务器都支持热部署,而对热部署的实现机制,网上讲的却不够完善,下面我们就Tomcat的热部署实现机制,讲授1下它是如何实现的:

Tomcat的容器实现热部署使用了两种机制:

Classloader重写,通过自定义classloader加载相应的jsp编译后的class到JVM中。 通过动态修改内存中的字节码,将修改过的class再次装载到JVM中。

Classloader实现jsp的重新加载

Tomcat通过org.apache.jasper.servlet.JasperLoader实现了对jsp的加载,下面做个测试: 1. 新建1个web工程,并编写1个jsp页面,在jsp页面中输出该页面的classloader,<%System.out.print(this.getClass().getClassLoader());%>.

2. 启动web服务器,打开jsp页面,我们可以看到后台输出,该jsp的classloader是JasperLoader的1个实例。

3. 修改jsp,保存并刷新jsp页面,再次查看后台输出,此classloader实例已不是刚才那个了,也就是说tomcat通过1个新的classloader再次装载了该jsp。

4. 其实,对每一个jsp页面tomcat都使用了1个独立的classloader来装载,每次修改完jsp后,tomcat都将使用1个新的classloader来装载它。

关于如何使用自定义classloader来装载1个class这里就不说了,相信网上都能找到,JSP属于1次性消费,每次调用容器将创建1个新的实例,属于用完就扔的那种,但是对这类实现方式却很难用于其它情况下,如现在我们工程中很多都使用了单例,特别是spring工程,在这类情况下使用新的classloader来加载修改后的类是不现实的,单例类将在内存中产生多个实例,而且这类方式没法改变当前内存中已有实例的行动,固然,tomcat也没通过该方式实现class文件的重新加载。

通过代理修改内存中class的字节码

Tomcat中的class文件是通过org.apache.catalina.loader. WebappClassLoader装载的,一样我们可以做个测试,测试进程与jsp测试类似,测试步骤就不说了,只说1下结果:

在热部署的情况下,对被该classloader 加载的class文件,它的classloader始终是同1个WebappClassLoader,除非容器重启了,相信做完这个实验你就不会再认为tomcat是使用1个新的classloader来加载修改过的class了,而且对有状态的实例,之前该实例具有的属性和状态都将保存,并在下次履行时具有了新的class的逻辑,这就是热部署的神秘的地方(其实每一个实例只是保存了该实例的状态属性,我们通过序列化对象就可以看到对象中包括的状态,终究的逻辑还是存在于class文件中)。

下面的class重定义是通过:java.lang.instrument实现的,具体可参考相干文档。

下面我们看1下如何通过代理修改内存中的class字节码:

以下是1个简单的热部署代理实现类(代码比较粗糙,也没甚么判断):

复制代码
package agent;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.util.Set;
import java.util.Timer;
import java.util.TreeSet;
public  class  HotAgent {
 
    protected  static  Set<String>  clsnames=new TreeSet<String>();
 
    public  static  void  premain(String  agentArgs, Instrumentation  inst)  throws Exception {
        ClassFileTransformer  transformer =new ClassTransform(inst);
        inst.addTransformer(transformer);
        System.out.println("是不是支持类的重定义:"+inst.isRedefineClassesSupported());
        Timer  timer=new  Timer();
        timer.schedule(new ReloadTask(inst),2000,2000);
    }
}

package agent;
import java.lang.instrument.ClassFileTransformer;
importjava.lang.instrument.IllegalClassFormatException;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
 
public  class  ClassTransform.  implements ClassFileTransformer {
    private  Instrumentation  inst;
 
    protected  ClassTransform(Instrumentation  inst){
        this.inst=inst;
    }
 
    /**
     * 此方法在redefineClasses时或初次加载时会调用,也就是说在class被再次加载时会被调用,
     * 并且我们通过此方法可以动态修改class字节码,实现类似代理之类的功能,具体方法可以使用ASM或javasist,
     * 如果对字节码很熟习的话可以直接修改字节码。
     */
    public  byte[]  transform(ClassLoader  loader, String  className,
           Class<?>  classBeingRedefined, ProtectionDomain  protectionDomain,
           byte[]  classfileBuffer)throws IllegalClassFormatException {
        byte[]  transformed = null;
        HotAgent.clsnames.add(className);
        return  null;
    }
}

package agent;
import java.io.InputStream;
import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.util.TimerTask;
 
public  class  ReloadTask  extends  TimerTask {
    private  Instrumentation  inst;
 
    protected  ReloadTask(Instrumentation  inst){
        this.inst=inst;
    }
 
    @Override
    public  void  run() {
       try{
           ClassDefinition[]  cd=new ClassDefinition[1];
           Class[]  classes=inst.getAllLoadedClasses();
           for(Class  cls:classes){
                if(cls.getClassLoader()==null||!cls.getClassLoader().getClass().getName().equals("sun.misc.Launcher$AppClassLoader"))
                    continue;
                String  name=cls.getName().replaceAll("\\.","/");
                cd[0]=new ClassDefinition(cls,loadClassBytes(cls,name+".class"));
                inst.redefineClasses(cd);
           }
       }catch(Exception ex){
            ex.printStackTrace();
       }
    }
 
    private  byte[]  loadClassBytes(Class  cls,String  clsname) throws  Exception{
        System.out.println(clsname+":"+cls);
        InputStream  is=cls.getClassLoader().getSystemClassLoader().getResourceAsStream(clsname);
        if(is==null)return  null;
        byte[]  bt=new  byte[is.available()];
        is.read(bt);
        is.close();
        return  bt;
    }
}
复制代码

 

以上是基本实现代码,需要组件为: 1.HotAgent(预加载) 2.ClassTransform(在加载class的时候可以修改class的字节码),本例中没用到 3.ReloadTask(class定时加载器,以上代码仅供参考) 4.META-INF/MANIFEST.MF内容为:(参数1:支持class重定义;参数2:预加载类)

Can-Redefine-Classes: true Premain-Class: agent.HotAgent

5.将以上组件打包成jar文件(到此,组件已完成,下面为编写测试类文件)。 6.新建1个java工程,编写1个java逻辑类,并编写1个Test类,在该测试类中调用逻辑类的方法,下面看下测试类代码:

 

复制代码
package test.redefine;
 
public  class  Bean1 {
    public  void  test1(){
      System.out.println("============================");
    }
}

package test.redefine;
 
public  class  Test {
    public  static  void  main(String[] args)throws  InterruptedException {
 
       Bean1  c1=new  Bean1();
       while(true){
           c1.test1();
           Thread.sleep(5000);
       }
    }
}
复制代码

运行测试类:

java –javaagent:agent.jar test.redefine.Test

在测试类中,我们使用了1个死循环,定时调用逻辑类的方法。我们可以修改Bean1中的方法实现,将在不同时间看到不同的输出结果,关于技术细节也没甚么好讲的了,相信大家都能明白。

Tomcat 热部署配置

 

复制代码
<Host appBase="webapps" autoDeploy="true" name="localhost" unpackWARs="true" xmlNamespaceAware="false" xmlValidation="false">  
    <Context docBase="CPCWeb" path="/CPCWeb" reloadable="true" source="org.<span class="wp_keywordlink"><a href="http://res.importnew.com/eclipse" title="Eclipse ImportNew主页" target="_blank">Eclipse</a></span>.jst.j2ee.server:CPCWeb"/>
</Host>


复制代码

autoDeploy=”true” — 自动部署 reloadable=”true” — 自动加载

生活不易,码农辛苦
如果您觉得本网站对您的学习有所帮助,可以手机扫描二维码进行捐赠
程序员人生
------分隔线----------------------------
分享到:
------分隔线----------------------------
关闭
程序员人生