tomcat热部署的实现原理

一.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字节码:

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

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 clsnames = new TreeSet();

	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热部署的实现原理)