我们用eclipse+tomcat开发web项目时,经常会这样做,修改个java文件,然后不需要重启tomcat就可以直接看到修改后的结果, 这就是热替换技术。
对热替换技术原理的探索源于hotswap,但是hotswap的限制比较多。用户手册:http://jm-blog.aliapp.com/?p=641
阿里自己实现了hotcode2,功能挺强大的,而且在不断的更新。
原理就是:切入java.lang.ClassLoader中的方法defineClass,通过asm将加载的类修改。比如在方法中加入代码,用于判断当前class是否修改过,如果修改过,就重新加载。
下面贴一张hotswap和hotcode2的比较图:
-------------------------------------------------------------------------------
接下来简单说下hot2hot:
git地址:https://github.com/liuxinglanyue/hot2hot
首先通过javagent的方式切入, AgentMain类做了两件事儿。第一,修改java.lang.ClassLoader中的defineClass方法。第二,定时扫描一个文件夹,替换class。
defineClass方法如下:
protected final Class> defineClass(String name, byte[] b, int off, int len, ProtectionDomain protectionDomain) throws ClassFormatError { //hot2hot if(!name.startsWith("sun.") && !name.startsWith("java.") && !name.startsWith("javax.") && !name.startsWith("org.omg.") && !name.startsWith("org.w3c.") && !name.startsWith("org.xml.") && !name.startsWith("sun.")) { protectionDomain = preDefineClass(name, protectionDomain); Class c = null; String source = defineClassSourceLocation(protectionDomain); try { c = defineClass1(name, b, off, len, protectionDomain, source); } catch (ClassFormatError cf) { c = defineTransformedClass(name, b, off, len, protectionDomain, cf, source); } postDefineClass(c, protectionDomain); Clazzs.setClazz(name, c); return c; } //hot2hot protectionDomain = preDefineClass(name, protectionDomain); Class c = null; String source = defineClassSourceLocation(protectionDomain); try { c = defineClass1(name, b, off, len, protectionDomain, source); } catch (ClassFormatError cfe) { c = defineTransformedClass(name, b, off, len, protectionDomain, cfe, source); } postDefineClass(c, protectionDomain); return c; }
Clazzs内部就是ConcurrentHashMap,修改defineClass的目的就是将Class信息保存起来。
修改defineClass方法的代码如下:
package org.hot2hot.jdk; import java.security.ProtectionDomain; import org.hot2hot.utils.Clazzs; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; /** * 修改ClassLoader的defineClass * @author jiaojianfeng * */ public class ClassLoaderAdapter extends ClassVisitor { public ClassLoaderAdapter(ClassVisitor cv){ super(Opcodes.ASM4, cv); } @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, desc, signature, exceptions); if (name.equals("defineClass") && desc.equals(Type.getMethodDescriptor(Type.getType(Class.class), Type.getType(String.class), Type.getType(byte[].class), Type.INT_TYPE, Type.INT_TYPE, Type.getType(ProtectionDomain.class)))) { return new MethodVisitor(Opcodes.ASM4, mv) { @Override public void visitCode() { super.visitCode(); //if 判断 Label jump = new Label(); Label tryLabel = new Label(); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("com.sun."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("java."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("javax."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("org.omg."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("org.w3c."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("org.xml."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitLdcInsn("sun."); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, Type.getInternalName(String.class), "startsWith", "(Ljava/lang/String;)Z"); mv.visitJumpInsn(Opcodes.IFNE, jump); //if mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitVarInsn(Opcodes.ALOAD, 5); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ClassLoader.class), "preDefineClass", "(Ljava/lang/String;Ljava/security/ProtectionDomain;)Ljava/security/ProtectionDomain;"); mv.visitVarInsn(Opcodes.ASTORE, 5); mv.visitInsn(Opcodes.ACONST_NULL); mv.visitVarInsn(Opcodes.ASTORE, 6); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 5); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ClassLoader.class), "defineClassSourceLocation", "(Ljava/security/ProtectionDomain;)Ljava/lang/String;"); mv.visitVarInsn(Opcodes.ASTORE, 7); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitVarInsn(Opcodes.ALOAD, 2); mv.visitVarInsn(Opcodes.ILOAD, 3); mv.visitVarInsn(Opcodes.ILOAD, 4); mv.visitVarInsn(Opcodes.ALOAD, 5); mv.visitVarInsn(Opcodes.ALOAD, 7); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ClassLoader.class), "defineClass1", "(Ljava/lang/String;[BIILjava/security/ProtectionDomain;Ljava/lang/String;)Ljava/lang/Class;"); mv.visitVarInsn(Opcodes.ASTORE, 6); mv.visitJumpInsn(Opcodes.GOTO, tryLabel); mv.visitVarInsn(Opcodes.ASTORE, 8); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitVarInsn(Opcodes.ALOAD, 2); mv.visitVarInsn(Opcodes.ILOAD, 3); mv.visitVarInsn(Opcodes.ILOAD, 4); mv.visitVarInsn(Opcodes.ALOAD, 5); mv.visitVarInsn(Opcodes.ALOAD, 8); mv.visitVarInsn(Opcodes.ALOAD, 7); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ClassLoader.class), "defineTransformedClass", "(Ljava/lang/String;[BIILjava/security/ProtectionDomain;Ljava/lang/ClassFormatError;Ljava/lang/String;)Ljava/lang/Class;"); mv.visitVarInsn(Opcodes.ASTORE, 6); mv.visitLabel(tryLabel); mv.visitVarInsn(Opcodes.ALOAD, 0); mv.visitVarInsn(Opcodes.ALOAD, 6); mv.visitVarInsn(Opcodes.ALOAD, 5); mv.visitMethodInsn(Opcodes.INVOKESPECIAL, Type.getInternalName(ClassLoader.class), "postDefineClass", "(Ljava/lang/Class;Ljava/security/ProtectionDomain;)V"); mv.visitVarInsn(Opcodes.ALOAD, 1); mv.visitVarInsn(Opcodes.ALOAD, 6); mv.visitMethodInsn(Opcodes.INVOKESTATIC, Type.getInternalName(Clazzs.class), "setClazz", "(Ljava/lang/String;Ljava/lang/Class;)V"); mv.visitVarInsn(Opcodes.ALOAD, 6); mv.visitInsn(Opcodes.ARETURN); mv.visitLabel(jump); } }; } return mv; } }
第二点的定时扫描文件,没什么难点。其中,根据class文件获取包名,前文已经分析过。具体参看github
说明下:hot2hot没有经过线上测试,使用请谨慎。