class热替换项目hot2hot


我们用eclipse+tomcat开发web项目时,经常会这样做,修改个java文件,然后不需要重启tomcat就可以直接看到修改后的结果, 这就是热替换技术。

对热替换技术原理的探索源于hotswap,但是hotswap的限制比较多。用户手册:http://jm-blog.aliapp.com/?p=641

阿里自己实现了hotcode2,功能挺强大的,而且在不断的更新。

原理就是:切入java.lang.ClassLoader中的方法defineClass,通过asm将加载的类修改。比如在方法中加入代码,用于判断当前class是否修改过,如果修改过,就重新加载。

下面贴一张hotswap和hotcode2的比较图:

hotcode

-------------------------------------------------------------------------------

接下来简单说下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没有经过线上测试,使用请谨慎。


上篇: 从class文件中获取包名(fast版本) 下篇: 加载javaagent的classloader问题