Java热加载 AgentMain 方式(Version:asm-all-5.0.3,log4j-1.2.17)

  • premain是Java SE5开始就提供的代理方式,由于其必须在命令行指定代理jar,并且代理类必须在main方法前启动。因此,要求开发者在应用前就必须确认代理的处理逻辑和参数内容等等。在有些场合下,premain代理方式不能满足需求:

    • premain 必须启动时在命令行指定代理jar,
    • 并且代理类必须在main方法前启动,只执行一次
    • 类加载之后无能为力,只能通过重新创建ClassLoader 这种方式重新加载
  • 为解决运行时启动代理类的问题,Java SE6开始提供了在应用程序的VM启动后在动态添加代理的方式,即agentmain方式。 agentmain 可以在类加载之后再次加载一个类,也就是重定义,你就可以通过在重定义的时候进行修改类了

  • 与Permain类似,agent方式同样需要提供一个agent jar,并且这个jar需要满足:

    • 代理类需要提供agentmain方法。并且再二者同时存在时以前者优先。args和inst和premain中的一致:
      public static void agentmain(String args, Instrumentation inst)
      public static void agentmain(String args)
      
    • 定义一个MANIFEST.MF 文件,文件中必须包含 Agent-Class
    • 创建一个 Agent-Class 指定的类,该类必须包含 agentmain 方法
    • 将MANIFEST.MF 和 Agent 类打成 jar 包
    • 将 jar 包载入目标虚拟机,目标虚拟机将会自动执行 agentmain 方法执行方法逻辑
  • 重定义类的对类的修改是有限制的:

    • 父类是同一个;
    • 实习那的接口数也要相同;
    • 类访问符必须一致;
    • 字段数和字段名必须一致;
    • 新增的方法必须是 private static/final 的;
    • 可以删除修改方法;
  • 计划:每一次类修改重加载后,给每个方法添加计时程序

  • 计时程序(注意:所有程序我都没有写包名)

    import org.apache.log4j.Logger;
    
    public class InvokeTimer {
      private static Logger logger = Logger.getLogger(InvokeTimer.class);
    
    
      static ThreadLocal<Long> threadLocal = new ThreadLocal<Long>();
    
    
      public static void start() {
          threadLocal.set(System.currentTimeMillis());
      }
    
      public static void end() {
          long time = System.currentTimeMillis() - threadLocal.get();
          StackTraceElement element = Thread.currentThread().getStackTrace()[2];
          logger.debug("Class name:"+element.getClassName() +",Method name:"+ element.getMethodName() + ",Line number:"+ element.getLineNumber()+ ",耗费时间:" + time + "ms.");
      }
    }
    
  • 获取app根路径

import java.io.InputStream;
import java.net.URL;

/**
 * Created by micocube
 * ProjectName: coding
 * PackageName: com.coding.Path
 * User: micocube
 * Email: ldscube@gmail.com
 * CreateTime: 2019/1/11下午4:01
 * ModifyTime: 2019/1/11下午4:01
 * Version: 0.1
 * Description:
 **/
public class PathUtils {
    public static void main(String[] args) {
        System.out.println(PathUtils.getRootPath());
    }

    public static String getRootPath(){
        URL resource = ClassLoader.getSystemResource("");
//        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//        URL resource = classLoader.getResource("");
        String path = resource.getPath();
//        System.out.println("RootPath:"+path);
        return path;
    }


    public static String getRootPath(String name){
        URL resource = ClassLoader.getSystemResource("");
//        URL resource = Thread.currentThread().getContextClassLoader().getResource(name);
        String path = resource.getPath();
        return path;
    }

    public static String getClassRealPath(Class clazz){
        String rootPath = getRootPath();
        String path = rootPath + class2ClassPath(clazz);
        return path;
    }

    private static String class2ClassPath(Class clazz) {
        return clazz.getName().replace('.', '/') + ".class";
    }


    public static InputStream getClassInputStream(Class clazz){
        InputStream is = Thread.currentThread().getContextClassLoader()
                .getResourceAsStream(class2ClassPath(clazz));

        return is;
    }
}

  • AgentMain 程序
import org.apache.log4j.Logger;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;


/**
 * Created by micocube
 * ProjectName: coding
 * PackageName: com.coding.instrumentation
 * User: micocube
 * Email: ldscube@gmail.com
 * CreateTime: 2019/1/17下午3:02
 * ModifyTime: 2019/1/17下午3:02
 * Version: 0.1
 * Description:
 * <p>
 * System.out.println("agentmain load Class  :" + className);
 * Class[] classes = inst.getInitiatedClasses(loader);
 * //                 获取所有已经被初始化过了的类
 * System.out.println("initiated classes:");
 * Arrays.stream(classes).forEach(System.out::println);
 * //                  获取所有已经被加载的类
 * Class[] allLoadedClasses = inst.getAllLoadedClasses();
 * System.out.println("loaded classes:");
 * Arrays.stream(allLoadedClasses).forEach(System.out::println);
 **/
public class HotSwapAgent {
    private static Logger logger = Logger.getLogger(HotSwapAgent.class);

    private static Map<String, Content> classes = new HashMap<String, Content>();

    public static void premain(String args, Instrumentation inst)
    {
        System.out.println("PreMain Args:" + args);
    }

    public static void agentmain(String agentArgs, Instrumentation inst){

//        Metric.printInfo();

        Class<?>[] allClass = inst.getAllLoadedClasses();
        String rootPath = PathUtils.getRootPath();
        Content.getAllClasses(rootPath, classes);
        logger.debug("Scan Modified Classes:\n");
        Set<String> clazz = classes.keySet();
        for(String cl : clazz){
            Content content = classes.get(cl);
            if(content.isModified()){
                System.out.println(content);
            }
        }
        for (Class<?> c : allClass) {
            //System.out.println("Loaded class:" + c.getName());
            if (classes.containsKey(c.getName())) {
                Content content = classes.get(c.getName());
                if (content.isModified()) {

                    byte[] classFile = content.getContent();
//                    if(c.getName().equals("TestService")){
                    ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
                    ClassReader reader = new ClassReader(classFile);
                    reader.accept(new AopClassAdapter(Opcodes.ASM5,cw), 0);
                    classFile = cw.toByteArray();
                        //new FileOutputStream("/Users/micocube/Documents/AgentMain/out/"+c.getName()+".class").write(classFile);
//                    }

                    ClassDefinition classDefinition = new ClassDefinition(c, classFile);
                    try {
                        inst.redefineClasses(classDefinition);
                    } catch (ClassNotFoundException e) {
                        e.printStackTrace();
                    } catch (UnmodifiableClassException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

//        HotSwapTransformer transformer = new HotSwapTransformer();
//        inst.addTransformer(transformer, true);
//        inst.retransformClasses(Account.class);
    }
}

  • ASM给method添加计时程序
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;


public class AopClassAdapter extends ClassVisitor implements Opcodes {
    public AopClassAdapter(int api, ClassWriter classWriter) {
        super(api, classWriter);
    }
    public void visit(int version, int access, String name,
                         String signature, String superName, String[] interfaces) {
        super.visit(version,access,name,signature,superName,interfaces);
    }
    public MethodVisitor visitMethod(int access, String name,
                                     String desc, String signature, String[] exceptions) {
        MethodVisitor mv = super.visitMethod(access, name,
                                          desc, signature, exceptions);
        MethodVisitor visitor = new MethodVisitor(this.api, mv) {
            /**
             * 访问方法头,只访问一次
             */
            @Override
            public void visitCode() {
                super.visitCode();
                this.visitMethodInsn(INVOKESTATIC, "InvokeTimer", "start", "()V",false);
            }

            /**
             * 返回
             * @param opcode
             */
            @Override
            public void visitInsn(int opcode) {
                if (opcode == RETURN) {//在返回之前安插after 代码。
                    mv.visitMethodInsn(INVOKESTATIC, "InvokeTimer", "end", "()V",false);
                }
                super.visitInsn(opcode);
            }
        };


        return visitor;
    }
}

  • 扫描app根路径,获取更改过的class文件
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.Map;

public class Content {

    private File file;
    private Long lastModified = -1L;
    private boolean modified = false;
    private byte[] content = new byte[0];
    public Content(File file) {
        this.file = file;
    }


    public static void getAllClasses(String rootPath, Map<String, Content> classes) {
        File file = new File(rootPath);
        File[] list = file.listFiles();
        for (int i = 0; i < list.length; i++) {
            File f = list[i];
            if (f.isDirectory()) {
                getAllClasses(f.getPath(), classes);
            }

            if (f.getPath().endsWith(".class")) {
                String path = f.getPath();
                String key = path.replace(PathUtils.getRootPath(), "").replace(".class", "");
                Content content;
                if (classes.containsKey(key)) {
                    content = classes.get(key);
                } else {
                    content = new Content(f);
                    classes.put(key, content);
                }
                try {
                    Content.fileContent(content);
                } catch (Exception e) {
                    System.out.println(e.getMessage());
                }
            }
        }

    }

    public static Content fileContent(Content content) throws Exception {
        File file = content.getFile();
        Long lastModified = content.getLastModified();
        if (lastModified == file.lastModified()) {
            content.setModified(false);
        } else {
            content.setModified(true);
            content.setLastModified(file.lastModified());
            InputStream inputStream = new FileInputStream((file));
            byte[] buffer = new byte[inputStream.available()];
            int read = inputStream.read(buffer);
            if (-1 == read) {
                System.out.println("empty file :" + file.getName());
                content.setContent(null);
            } else {
                content.setContent(buffer);
            }
        }
        return content;
    }

    public boolean isModified() {
        return modified;
    }

    public Content setModified(boolean modified) {
        this.modified = modified;
        return this;
    }

    public File getFile() {
        return file;
    }

    public Content setFile(File file) {
        this.file = file;
        return this;
    }

    public Long getLastModified() {
        return lastModified;
    }

    public Content setLastModified(Long lastModified) {
        this.lastModified = lastModified;
        return this;
    }

    public byte[] getContent() {
        return content;
    }

    public Content setContent(byte[] content) {
        this.content = content;
        return this;
    }

    @Override
    public String toString() {
        final StringBuffer sb = new StringBuffer("Content{\n");
        sb.append("file=").append(file).append(",\n");
        sb.append("lastModified=").append(lastModified).append(",\n");
        sb.append("modified=").append(modified).append(",\n");
        sb.append("contentLength=").append(content.length).append("\n");
        sb.append("}\n");
        return sb.toString();
    }
}
  • 逻辑很简单 :

    • 获取所有已经加载过的类,
    • 扫描app的根路径,检测是否有更改过的class文件
    • 如果class文件被更改,使用ASM给该类的所有method添加计时程序
    • 调用Instrumentation的redefineClasses方法,重定义类
  • 执行程序:

    • 模拟app运行期间的执行程序[把它想象成tomcat运行用户定义的方法向外提供服务的app]
    public class Main {
      public static void main(String[] args) throws InterruptedException {
          for (; ; ) {
              new TestService().operation();
              Thread.sleep(5000);
          }
    
      }
    }
    
    public class TestService {
      public void operation() {
          System.out.println("Hello World!");
          try {
              Thread.sleep(10);
          } catch (Exception e) {
              e.printStackTrace();
          }
      }
    }
    
    • 使用外部程序定时加载agentmain的jar,每隔3秒扫描一次class
      import com.sun.tools.attach.*;
      import java.io.IOException;
      import java.util.List;
      import java.util.Timer;
      import java.util.TimerTask;
    
      /**
       * Created by micocube
       * ProjectName: coding
       * PackageName: com.coding.instrumentation
       * User: micocube
       * Email: ldscube@gmail.com
       * CreateTime: 2019/1/17下午3:15
       * ModifyTime: 2019/1/17下午3:15
       * Version: 0.1
       * Description:
       **/
      public class LoadAgent {
    
          public static void main(String[] args) throws Exception {
    
              new Timer().schedule(new TimerTask() {
                  @Override
                  public void run() {
                      try {
                          LoadAgent.load("Main",
                                  "/Users/micocube/Documents/AgentMain/src/AgentMain.jar",
                                  "cxs");
    
                      } catch (IOException e) {
                          e.printStackTrace();
                      } catch (AgentLoadException e) {
                          e.printStackTrace();
                      } catch (AgentInitializationException e) {
                          e.printStackTrace();
                      } catch (AttachNotSupportedException e) {
                          e.printStackTrace();
                      }
                  }
              },0,3000L);
    
    
          }
    
          public static void load(String appName, String agentJarPath, String agentArgs) throws IOException, AgentLoadException, AgentInitializationException, AttachNotSupportedException {
              System.out.println("agentJarPath"+agentJarPath+",args:"+agentArgs);
              List<VirtualMachineDescriptor> list = VirtualMachine.list();
              for (VirtualMachineDescriptor vmd : list) {
                  System.out.println("VirtualMachineDescriptorName:" + vmd.displayName());
                  if (vmd.displayName().endsWith(appName)) {
                      VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id());
                      virtualMachine.loadAgent(agentJarPath, agentArgs);
                      virtualMachine.detach();
                  }
              }
          }
      }
    
    
  • MANIFEST.MF,将该文件和上面所有的class打包成jar

Manifest-Version: 1.0
Can-Redefine-Classes: true
Agent-Class: HotSwapAgent
Premain-Class: HotSwapAgent
Can-Retransform-Classes: true
  • 运行

    • 运行Main.java
    • 运行LoadAgent.java,注意agentJarPath参数,换成你的jar的路径
    • 输出Hello World!
    • 修改TestService.operation方法,修改为输出Hello World! QQQQQ,重新编译,覆盖旧class,3秒后class被检测到修改,输出了新的内容
  • 所有代码

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,837评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,551评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,417评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,448评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,524评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,554评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,569评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,316评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,766评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,077评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,240评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,912评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,560评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,176评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,425评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,114评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,114评论 2 352

推荐阅读更多精彩内容

  • 一、流的概念和作用。 流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总成或抽象。即数据在两设备之间的传输...
    布鲁斯不吐丝阅读 10,027评论 2 95
  • 小编费力收集:给你想要的面试集合 1.C++或Java中的异常处理机制的简单原理和应用。 当JAVA程序违反了JA...
    八爷君阅读 4,585评论 1 114
  • 她到底喜不喜欢我? 她心里究竟在想什么? 我到底要不要继续? 这时候该不该表白? 我究竟用些什么招才能把对方搞到手...
    泡过的第一千零二个妞阅读 519评论 0 3
  • 匆匆地来, 匆匆地走。 甩一甩头, 除了记忆, 还有那两个海星之语的, 项链。 站在风景如画的, 大明湖畔。 陷入...
    凌雨星辰阅读 251评论 0 0
  • 文:E晴的叨叨 图:来自网络搜索 有人说:“人生如巧克力,没有人知道下一块说什么!” 贾静雯曾经的婚姻家暴新闻,因...
    苏茉以安阅读 396评论 0 1