Arthas
Arthas 是阿里的一款线上监控诊断工具,最近因为在排查一个bug用到了它,记录以下:
启动
curl -O https://arthas.aliyun.com/arthas-boot.jar
java -jar arthas-boot.jar
反编译
jad com.chengmq.xxx修改日志级别:
查看当前类的日志对象 找到 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里面的内容(双亲委派原则),发生异常。
参考文章:
又一次被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 查看前后变化。