当前位置: 移动技术网 > 网络运营>服务器>tomcat > Tomcat 热部署的实现原理详解

Tomcat 热部署的实现原理详解

2019年04月17日  | 移动技术网网络运营  | 我要评论
tomcat热部署机制  对于java应用程序来说,热部署就是在运行时更新java类文件。在基于java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色

tomcat热部署机制

 对于java应用程序来说,热部署就是在运行时更新java类文件。在基于java的应用服务器实现热部署的过程中,类装入器扮演着重要的角色。大多数基于java的应用服务器,包括ejb服务器和servlet容器,都支持热部署。类装入器不能重新装入一个已经装入的类,但只要使用一个新的类装入器实例,就可以将类再次装入一个正在运行的应用程序。

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

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

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

classloader实现jsp的重新加载

tomcat通过org.apache.jasper.servlet.jasperloader实现了对jsp的加载,

下面做个测试:

1. 新建一个web工程,并编写一个jsp页面,在jsp页面中输出该页面的classloader,.

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

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

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

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

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

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

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

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

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

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

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内容为:(参数一:支持class重定义;参数二:预加载类)

can-redefine-classes: true premain-class: agent.hotagent

5.将以上组件打包成jar文件(到此,组件已经完成,下面为编写测试类文件)。

6.新建一个java工程,编写一个java逻辑类,并编写一个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

在测试类中,我们使用了一个死循环,定时调用逻辑类的方法。我们可以修改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” — 自动加载

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

如您对本文有疑问或者有任何想说的,请点击进行留言回复,万千网友为您解惑!

相关文章:

验证码:
移动技术网