Tomcat热部署的实现原理

概述

名词解释:所谓热部署,就是在应用正在运行的时候升级软件,却不需要重新启动应用。

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

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

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

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

Classloader实现jsp的重新加载

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

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

import java.lang.instrument.ClassFileTransformer;
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);
    }
}
import java.lang.instrument.ClassFileTransformer;
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;
    }
}
import java.lang.instrument.ClassDefinition;
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中的方法实现,将在不同时间看到不同的输出结果,关于技术细节也没什么好讲的了,相信大家都能明白。

时间: 2024-08-10 01:55:42

Tomcat热部署的实现原理的相关文章

tomcat源码解读(1)–tomcat热部署实现原理

tomcat的热部署实现原理:tomcat启动的时候会有启动一个线程每隔一段时间会去判断应用中加载的类是否发生变法(类总数的变化,类的修改),如果发生了变化就会把应用的启动的线程停止掉,清除引用,并且把加载该应用的WebappClassLoader设为null,然后创建一个新的WebappClassLoader来重新加载应用. tomcat中热部署发现类变法之后要做的一系列停止工作的时序图如下: 上面时序图中只把关键的流转步骤画出来了,还有一些细节的处理没有完全画出来,这部分代码的继承的结构还是

Tomcat热部署,Web工程中线程没有终止

近期项目中,用 jenkins 热部署 web工程时,发现工程中静态持有的线程(将ScheduledExecutorService定时任务存储在静态Map中),导致不定时出现数据库访问事务关闭异常,如下:org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction; nested exception is java.lang.Il

IDEA第二章----配置git、tomcat(热部署)、database,让你的项目跑起来

第一节:下载git客户端,整合idea 由于博主公司用的git版本管理,所以本系列都是基于git版本工具的,当然SVN与git配置类似.git同样支持安装版和解压版,支持各种操作系统,我这里下载的是Windows的解压版. 选择刚才解压后的文件夹,选择cmd文件夹下的git.exe,然后点击Test查看是否连接成功. 注:如果没有配置git客户端,用git地址导入项目会提示你找不到git.exe. 第二节:配置tomcat(热部署稍后会讲到) tomcat下载安装就不在累赘,博主用的是tomca

jrebel+eclipse+tomcat热部署

jrebel+eclipse+tomcat热部署 搞了个jrebel准备热部署代码,方便以后开发web类型项目,网上找了一堆安装教程,各式各样的都有,尝试了几个,结果遇到了一堆问题,决定把正确流程贴出来. 本流程是按照jrebel官网安装,但jrebel是收费的(虽然说有一个免费social版,但我没有搞出来),期间加入破解方法. 第一步:下载jrebel tomcat和eclipse的安装就不说了,打开eclipse,打开help->Eclipse Marketplace,搜索jrebel,点

idea tomcat热部署 Error running &#39;Tomcat 7&#39;: Unable to open debugger port (127.0.0.1:3622): java.net.SocketExcepti

今天在进 tomcat 的 debug 模式时报了此异常, tomcat 进入 debug 模式失败 网上查了下原因,发现通过修改下面两个端口即可正常进入 tomcat 的 debug 模式 idea tomcat热部署 Error running 'Tomcat 7': Unable to open debugger port (127.0.0.1:3622): java.net.SocketExcepti 原文地址:https://www.cnblogs.com/kinome/p/89899

IDEA中Tomcat热部署不生效问题解决办法

IDEA中Tomcat热部署不生效问题解决办法 1.设置完热部署后 2.一定要在Debug模式下运行不要点RUN!!!!!!!!!!!!!!!!! 原文地址:https://www.cnblogs.com/wenqiangit/p/11023658.html

Tomcat热部署的三种方式

热部署是指在你修改项目BUG的时候对JSP或JAVA类进行了修改在不重启WEB服务器前提下能让修改生效.但是对配置文件的修改除外! 1.直接把项目web文件夹放在webapps里. 2.在tomcat\conf\server.xml中的<host></host>内部添加<context/>标签: <Context debug="0" docBase="D:\demo1\web" path="/demo1"

Java服务器热部署的实现原理

转自:http://blog.csdn.net/chenjie19891104/article/details/42807959 在web应用开发或者游戏服务器开发的过程中,我们时时刻刻都在使用热部署.热部署的目的很简单,就是为了节省应用开发和发布的时间.比如,我们在使用Tomcat或者Jboss等应用服务器开发应用时,我们经常会开启热部署功能.热部署,简单点来说,就是我们将打包好的应用直接替换掉原有的应用,不用关闭或者重启服务器,一切就是这么简单.那么,热部署到底是如何实现的呢?在本文中,我将

Netty+Tomcat热部署端口占用解决办法(转)

在eclipse使用maven deploy (tomcat:deploy) 热部署netty项目 ,项目启动的时候会报错端口被占用. Java代码   java.net.BindException: Address already in use at sun.nio.ch.Net.bind0(Native Method) at sun.nio.ch.Net.bind(Net.java:444) at sun.nio.ch.Net.bind(Net.java:436) at sun.nio.ch