Arthas

Arthas 是阿里的一款线上监控诊断工具,最近因为在排查一个bug用到了它,记录以下:

官方文档:简介 | arthas (aliyun.com)

启动

curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar

反编译

jad com.chengmq.xxx

jad | arthas (aliyun.com)

修改日志级别:

查看当前类的日志对象 找到 classLoaderHash :

logger -n “class路径:com.abc.xxx“

找到当前类的classLoaderHash 的值

修改命令:

logger -c ”classLoaderHash “ -n ”class路径:com.abc.xxx“ --level debug

Arthas 还有其他的一些用法,例如:监控实例变量、查看方法耗时 等等 ,命令拼起来还是有一些复杂的,可以使用IDEA插件:arthas-idea-plugin 快速获取命令,太香了。

arhtas idea plugin 使用手册 (yuque.com)

使用 ReTransformer 还可以方便的更新开发或者测试环境的代码,用于解决紧急的问题。

灵机一动:既然线上的代码可以热更新,那么本地是否也可以呢?

查看介绍authas目前只支持Linux环境热更新,并且命令行粘贴对于线上环境来说是节省时间的,但对于本地来说,我们倾向于能不能一个命令搞定热更新呢。

答案是可以的,了解到有JRebel,但是它是付费的,付费不考虑。于是发现了HotSwapAgent - IntelliJ IDEs Plugin | Marketplace (jetbrains.com) 插件。它基于 For Java8: jdk8-dcevm + HotswapAgent 目前日常还是用的JDK8 ,正好适合。

安装过程就不细说了,大体上就是 需要先安装 dcevm,它需要绑定特定的JDK,笔者使用的最新的181. 安装完之后下载agent配置上就行了。

这里主要说下遇到的一个坑:

当使用idea运行工程时,不可以使用classpath file 方式,该方式项目依赖的jar包会使用 URLClassLoader (父类直接为引导类加载器)进行加载,而 agent 的 类加载器是固定的为:AppClassLoader 。这导致在应用运行的时候,加载不到 agent jar里面的内容(双亲委派原则),发生异常。

参考文章:

Java字节码增强探秘 (qq.com)

又一次被idea坑了(Shorten command line) - 简书 (jianshu.com)

使用 Hotswap 插件时,发现spring插件会导致启动异常缓慢,禁用了一些插件,使用分号分隔:

Hibernate;HibernateJakarta;Hibernate3JPA;Hibernate3;Jersey1;Jersey2;Jetty;ZK;Logback;Log4j2;MyFaces;Mojarra;Omnifaces;ELResolver;WildFlyELResolver;OsgiEquinox;Owb;OwbJakarta;Proxy;WebObjects;Weld;WeldJakarta;JBossModules;ResteasyRegistry;Deltaspike;GlassFish;Weblogic;Vaadin;Wicket;CxfJAXRS;FreeMarker;Undertow;MyBatis;IBatis;JacksonPlugin;Idea;Thymeleaf;Velocity;spring

看起来主要应该时spring插件影响的。日常使用的话能够修复方法基本上也够用了。

另外看到了美团关于这方面的文章:

Java系列 | 远程热部署在美团的落地实践 - 美团技术团队 (meituan.com)

好像没有开源,应该用不了。

温故而知新,回滚下之前学习的关于instrument 字节码增强的内容。

主要基于 agentmain(对应Attach),premain (对应java agent 方式)进行字节码增强:

import java.lang.management.ManagementFactory;

/** The type Base. 参考<a href="https://mp.weixin.qq.com/s/CH9D-E7fxuu462Q2S3t0AA">...</a> */
public class Base {

  public static void main(String[] args) {

    String name = ManagementFactory.getRuntimeMXBean().getName();
    String s = name.split("@")[0];
    // 打印当前Pid
    System.out.println("pid:" + s);
    while (true) {
      try {
        Thread.sleep(5000L);
      } catch (Exception e) {
        break;
      }

      new Base().process();
    }
  }



import java.lang.instrument.Instrumentation;

/**
 *
 * 还需要定义一个Agent,借助Agent的能力将Instrument注入到JVM中。
 *  我们将在下一小节介绍Agent,现在要介绍的是Agent中用到的另一个类Instrumentation。
 *      在JDK 1.6之后,Instrumentation可以做启动后的Instrument、本地代码(Native Code)的Instrument,
 *          以及动态改变Classpath等等。我们可以向Instrumentation中添加上文中定义的Transformer,并指定要被重加载的类,代码如下所示。
 *              这样,当Agent被Attach到一个JVM中时,就会执行类字节码替换并重载入JVM的操作。
 *
 *  https://blog.csdn.net/zheng12tian/article/details/40617345
 *
 *      1.主要API(java.lang.instrutment)
 *       1)ClassFileTransformer:定义了类加载前的预处理类,可以在这个类中对要加载的类的字节码做一些处理,譬如进行字节码增强
 *       2)Instrutmentation:增强器,由JVM在入口参数中传递给我们,提供了如下的功能
 *  addTransformer/ removeTransformer:注册/删除ClassFileTransformer
 *  retransformClasses:对于已经加载的类重新进行转换处理,即会触发重新加载类定义,需要注意的是,新加载的类不能修改旧有的类声明,譬如不能增加属性、不能修改方法声明
 *  redefineClasses:与如上类似,但不是重新进行转换处理,而是直接把处理结果(bytecode)直接给JVM
 *  getAllLoadedClasses:获得当前已经加载的Class,可配合retransformClasses使用
 *  getInitiatedClasses:获得由某个特定的ClassLoader加载的类定义
 *  getObjectSize:获得一个对象占用的空间,包括其引用的对象
 *  appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch:增加BootstrapClassLoader/SystemClassLoader的搜索路径
 *  isNativeMethodPrefixSupported/setNativeMethodPrefix:支持拦截Native Method
 *
 */
public class TestAgent {

    /**
     * 以Attach的方式载入,在Java程序启动后执行
     */
    public static void agentmain(String args, Instrumentation inst) {
        //指定我们自己定义的Transformer,在其中利用Javassist做字节码替换

        inst.addTransformer(new TestTransformer(), true);

        try {
            //重定义类并载入新的字节码
            inst.retransformClasses(Base.class);

            System.out.println("Agent Load Done.");

        } catch (Exception e) {
            System.out.println("agent load failed!");
        }
    }

    /**
     * 以vm参数的方式载入,在Java程序的main方法执行之前执行
     *  -javaagent:D:\WorkeSpace\ASM-Study\target\ASM-Study-1.0-SNAPSHOT.jar
     */
    public static void premain(String args, Instrumentation inst) {
        //指定我们自己定义的Transformer,在其中利用Javassist做字节码替换

        System.out.println("==========premain========");


        inst.addTransformer(new TestTransformer(), true);

        System.out.println("Agent Load Done.");

    }
}


import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

/**
 * 。要使用Instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。
 * 接口中的transform()方法会在类文件被加载时调用,而在Transform方法里,
 * 我们可以利用ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
 *
 */
public class TestTransformer implements ClassFileTransformer {

  public byte[] transform(
      ClassLoader loader,
      String className,
      Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer) {

    System.out.println("ClassLoader:" + loader);

    System.out.println("Transforming " + className);

    if (!className.equals("asm/asm/Base")) {
      return classfileBuffer;
    }

    try {
      ClassPool cp = ClassPool.getDefault();
      CtClass cc = cp.get("asm.asm.Base");
      CtMethod m = cc.getDeclaredMethod("process");

      m.insertBefore("{ System.out.println(\"start\"); }");
      m.insertAfter("{ System.out.println(\"end\"); }");

      return cc.toBytecode();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }
}

import com.sun.tools.attach.VirtualMachine;

public class Attacher {

    public static void main(String[] args) throws Exception {
        // 传入目标 JVM pid
        VirtualMachine vm = VirtualMachine.attach("65120");

        vm.loadAgent("target/javaAgent-0.0.1-SNAPSHOT.jar");

    }
}

import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

/**
 * 。要使用Instrument的类修改功能,我们需要实现它提供的ClassFileTransformer接口,定义一个类文件转换器。
 * 接口中的transform()方法会在类文件被加载时调用,而在Transform方法里,
 * 我们可以利用ASM或Javassist对传入的字节码进行改写或替换,生成新的字节码数组后返回。
 *
 */
public class TestTransformer implements ClassFileTransformer {

  public byte[] transform(
      ClassLoader loader,
      String className,
      Class<?> classBeingRedefined,
      ProtectionDomain protectionDomain,
      byte[] classfileBuffer) {

    System.out.println("ClassLoader:" + loader);

    System.out.println("Transforming " + className);

    if (!className.equals("asm/asm/Base")) {
      return classfileBuffer;
    }

    try {
      ClassPool cp = ClassPool.getDefault();
      CtClass cc = cp.get("asm.asm.Base");
      CtMethod m = cc.getDeclaredMethod("process");

      m.insertBefore("{ System.out.println(\"start\"); }");
      m.insertAfter("{ System.out.println(\"end\"); }");

      return cc.toBytecode();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }
}

运行 Base 和 Attacher 查看前后变化。