动态追踪技术底层分析
什么是动态追踪
-
不用关闭java程序重启,无侵入式的实现,即可统计java程序的运行处理信息
-
通过java agent技术实现
Java Agent 技术
-
JVM级别的aop
- 事前、事后、事中
-
比如要打印方法的入参和出参,此时是需要对java代码进行修改的,但是java程序已经运行了,数据就在运行时数据区中,而class文件就在方法区中,如果要改变某一个方法,就需要替换class文件,修改相应的字节码
-
一个JVM只能调用一个arthas
main方法
- premain方法
- agentmain方法,arthas就是使用的这种
premain实例
agent的实际项目
-
package com.example.javaagent.app; //VM参数中加入:-javaagent:F:\work_vip\javaagent-demo\agent\target\agent-1.0-SNAPSHOT.jar public class MainRun { public static void main(String[] args) { hello("world"); } private static void hello(String name) { System.out.println("hello " + name ); try { Thread.sleep(Integer.MAX_VALUE);//线程休眠 } catch (InterruptedException e) { e.printStackTrace(); } } }
agent构建步骤
-
1.编写agent
-
AgentApp类中包含premain方法和agentmain方法
-
package com.example.javaagent; import java.lang.instrument.Instrumentation; /** * instrument 一共有两个 main 方法,一个是 premain,另一个是 agentmain * 但在一个 JVM 中,只会调用一个 */ public class AgentApp { //在main 执行之前的修改 public static void premain(String agentOps, Instrumentation inst) { System.out.println("==============enter premain=============="); //System.out.println(agentOps); inst.addTransformer(new Agent()); } //控制类运行时的行为 public static void agentmain(String agentOps, Instrumentation inst) { System.out.println("==============enter agentmain=============="); } }
-
-
2.编写transformer
-
package com.example.javaagent; import javassist.ClassPool; import javassist.CtClass; import javassist.CtMethod; import java.lang.instrument.ClassFileTransformer; import java.lang.instrument.IllegalClassFormatException; import java.security.ProtectionDomain; public class Agent implements ClassFileTransformer { @Override public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { String loadName = className.replaceAll("/", "."); // System.out.println(className); if (className.endsWith("MainRun")) { try { //javassist 完成字节码增强(打印方法的执行时间<纳秒>) CtClass ctClass = ClassPool.getDefault().get(loadName); CtMethod ctMethod = ctClass.getDeclaredMethod("hello"); ctMethod.addLocalVariable("_begin", CtClass.longType); ctMethod.insertBefore("_begin = System.nanoTime();"); ctMethod.insertAfter("System.out.println(System.nanoTime() - _begin);"); return ctClass.toBytecode(); } catch (Exception e) { e.printStackTrace(); } } return classfileBuffer; } }
-
去实现一个class文件变异的接口
-
对MainRun这个类进行变异,同时对hello方法进行变异,增加了一个局部变量-----当前系统时间,并在方法执行前插入,同时在方法结束后打印这个参数和当前时间的差值,也就是方法的执行时间
-
-
3.打包
扫描二维码关注公众号,回复: 13117115 查看本文章-
在\resources\META-INF目录下,新建一个MANIFEST.MF文件
Manifest-Version: 1.0 Can-Redefine-Classes: true Can-Retransform-Classes: true premain-class: com.example.javaagent.AgentApp agentmain-class: AgentApp
-
在maven配置中增加,避免在idea自带maven打包时mf文件被替换
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <configuration> <archive> <manifestFile>src/main/resources/META-INF/MANIFEST.MF</manifestFile> </archive> </configuration> </plugin> </plugins> </build>
-
然后在agent项目里执行mvn install
-
Agentmain实例
- 这种模式一般用在一些诊断工具上。使用 jdk/lib/tools.jar 中的工具类中的 Attach API,可以动态的为运行中的程序加入一些功能。它的主要运行步骤如下:
- 获取机器上运行的所有 JVM 进程 ID;
- 选择要诊断的 jvm,选择进程id
- 将 jvm 使用 attach 函数链接上;
- 使用 loadAgent 函数加载 agent,动态修改字节码;
- 卸载 jvm。
- 这种模式不同于premain方法,premain方法所有的干预都会显示到被监控的程序里面,而agentmain则不会,它不会影响监控程序的输出,而是会将坚决信息回传回agentmain的监控程序,并显示在监控程序里面,arthas就是如此,例如使用arthas的watch命令,最终的监控结果会在arthas自己的命令行界面显示
Java Attach API
实现方法1
-
package ex10.attach; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; import java.util.Properties; //Java Attach API public class AttachDemo { public static void main(String[] args) throws Exception { //VM进程号,通过 jps命令获取 //attach向目标 JVM ”附着”(Attach)代理工具程序 VirtualMachine vm = VirtualMachine.attach("8900"); // get system properties in target VM Properties props = vm.getSystemProperties(); String version = props.getProperty("java.version"); System.out.println(version); // VirtualMachineDescriptor是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能 List<VirtualMachineDescriptor> vmDescriptors = vm.list(); //从JVM上面解除代理 vm.detach(); } }
-
注意,使用上述代码,需要引入E:\Java\JDK\lib\tools.jar这个jar包
实现方法2
-
package ex10.attach; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; import java.util.Properties; import java.util.Set; /** * @author King老师 * Attach使用入门 */ public class JvmAttach { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { //只找对应启动类是JVMObject结尾的 if (vmd.displayName().endsWith("JVMObject")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); Properties props = virtualMachine.getSystemProperties(); //打印attach上的VM所有的系统属性 System.out.println(getChildProcessConfig(props)); //打印attach上的VM的JDK版本信息 String version = props.getProperty("java.version"); System.out.println("----version:"+version); virtualMachine.detach(); } } } //获取所有属性 private static Properties getChildProcessConfig( Properties props) { Properties properties = System.getProperties(); Set<String> stringPropertyNames = properties.stringPropertyNames(); Properties prop = new Properties(); for (String string : stringPropertyNames) { prop.setProperty(string, properties.getProperty(string)); } return prop; } }
Instrument
实战案例
-
package com.example.javaagent.app; import com.sun.tools.attach.VirtualMachine; import com.sun.tools.attach.VirtualMachineDescriptor; import java.util.List; import java.util.Properties; public class JvmAttach { public static void main(String[] args) throws Exception { List<VirtualMachineDescriptor> list = VirtualMachine.list(); for (VirtualMachineDescriptor vmd : list) { if (vmd.displayName().endsWith("MainRun")) { VirtualMachine virtualMachine = VirtualMachine.attach(vmd.id()); Properties props = virtualMachine.getSystemProperties(); String version = props.getProperty("java.version"); virtualMachine.loadAgent("arthas-boot.jar ","..."); System.out.println("version:"+version); virtualMachine.detach(); } } } }
-
获取对应方法的jvm,并加载对应的代理jar包,然后去修改字节码
-
这里是利用arthas实现的,例如arthas的watch方法,自己会把监视的结果回传回来,这是底层实现的,代码隐藏了,但是本质就是通过网络通讯回传回来的
总结
-
上述只模拟了一个premain的方法,agentmain的模式没有模拟,只是以springboot项目为基础,拿arthas演示了一下watch方法,上面的实战实例和java attach api内容可以不自己看,如果想仔细看,去看第二期的内容,而模拟的springboot项目的部分内容如下
-
package cn.enjoyedu.demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 类说明: */ @RestController @RequestMapping("/btrace") public class DemoController { @RequestMapping("/test") public String test(@RequestParam("name") String name){ return "hello,"+name; } @RequestMapping("/exception") public String exception(){ try { System.out.println("start......."); System.out.println(1/0); System.out.println("end........."); } catch (Exception e) { } return "success"; } }
-
借助Btrace手写动态追踪框架
-
出现了非常久了,但是对技术要求很高,而arthas使用门槛很低
-
在arthas诞生之前,都是通过Btrace实现追踪
-
btrace的github地址:
- https://github.com/btraceio/btrace,
BTrace基于ASM、Java Attach API、Instrument开发 - ASM是字节码增强工具,但是接口比较难懂,像CGLIB就是基于ASM实现的,ASM可以在JVM运行过程中动态创建一个class文件
- https://github.com/btraceio/btrace,
实战演练
-
下载Btrace,并配置环境变量(类似jdk)
-
命令行输入btrace,显示内容则成功
-
1.仍然以springboot哪个hello,test项目为例
-
DemoController
package cn.enjoyedu.demo.controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; /** * 类说明: */ @RestController @RequestMapping("/btrace") public class DemoController { @RequestMapping("/test") public String test(@RequestParam("name") String name){ return "hello,"+name; } @RequestMapping("/exception") public String exception(){ try { System.out.println("start......."); System.out.println(1/0); System.out.println("end........."); } catch (Exception e) { } return "success"; } }
-
-
2.需要引入btrace的三个类
-
TestBrace
package cn.enjoyedu.btrace; import com.sun.btrace.AnyType; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.str; import static com.sun.btrace.BTraceUtils.strcat; /** * 类说明:检查方法的输入和输出 */ @BTrace public class TestBrace { // 跟踪的方法 @OnMethod( clazz = "cn.enjoyedu.demo.controller.DemoController", method = "test", location = @Location(Kind.ENTRY) ) public static void checkEntry( @ProbeClassName String pcn, @ProbeMethodName String pmn, AnyType[] args ){ //打印了方法的参数 //BTraceUtils这个类就具有远程打印功能,就是在BTrace本身打印,而不是在springboot那个项目里面打印 BTraceUtils.println("Class: "+pcn); BTraceUtils.println("Method: "+pmn); BTraceUtils.printArray(args); BTraceUtils.println("==========================="); BTraceUtils.println(); } @OnMethod( clazz = "cn.enjoyedu.demo.Service.NormalService", method = "getBoolean",/*这里需要修改*/ location = @Location(Kind.RETURN)/*这里需要修改*/ ) public static void checkReturn(/*这里需要修改*/ @ProbeClassName String pcn, @ProbeMethodName String pmn, @Return boolean result /*这里需要修改*/ ){ BTraceUtils.println("Class: "+pcn); BTraceUtils.println("Method: "+pmn); BTraceUtils.println(strcat("result:",str(result)));/*这里需要修改*/ BTraceUtils.println("==========================="); BTraceUtils.println(); } }
-
MoreBtrace
package cn.enjoyedu.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.*; import static com.sun.btrace.BTraceUtils.*; /** * 类说明: */ @BTrace public class MoreBtrace { // 在testBtrace的实例中加入很多的功能 // 注解写的更复杂了 // Location是动作,方法调用 // clazz、method是正则规则 // where是after之后 @OnMethod( clazz = "cn.enjoyedu.demo.controller.DemoController", method = "test", location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/", where = Where.AFTER)) public static void onInvoke(@Self Object self, @TargetInstance Object instance, @TargetMethodOrField String method, @Duration long duration){ BTraceUtils.println(strcat("self: ", str(self))); BTraceUtils.println(strcat("instance: ", str(instance))); BTraceUtils.println(strcat("method: ", str(method))); BTraceUtils.println(strcat("duration(ns): ", str(duration ))); println("==========================="); BTraceUtils.println(); } @OnMethod( clazz = "cn.enjoyedu.demo.controller.DemoController", location = @Location(value = Kind.LINE, line = 26)) public static void onBind() { println("execute line 20"); println("---------------------------"); BTraceUtils.println(); } @OnMethod( clazz = "/cn\\.enjoyedu\\.demo\\.controller\\..*/", method = "/.*/", location = @Location(Kind.RETURN)) public static void slowQuery(@ProbeClassName String pcn, @ProbeMethodName String probeMethod, @Duration long duration){ if(duration > 1000000 * 100){ println(strcat("class:", pcn)); println(strcat("method:", probeMethod)); println(strcat("duration:", str(duration / 1000000))); println("*************************"); BTraceUtils.println(); } } }
-
TraceException
package cn.enjoyedu.btrace; import com.sun.btrace.BTraceUtils; import com.sun.btrace.annotations.*; /** * 类说明: */ @BTrace public class TraceException { @TLS static Throwable currentException; // introduce probe into every constructor of java.lang.Throwable // class and store "this" in the thread local variable. @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow(@Self Throwable self) { // @Self其实就是拦截了this //new Throwable() currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s) { //new Throwable(String msg) currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow1(@Self Throwable self, String s, Throwable cause) { //new Throwable(String msg, Throwable cause) currentException = self; } @OnMethod( clazz="java.lang.Throwable", method="<init>" ) public static void onthrow2(@Self Throwable self, Throwable cause) { //new Throwable(Throwable cause) currentException = self; } // when any constructor of java.lang.Throwable returns // print the currentException's stack trace. @OnMethod( clazz = "cn.enjoyedu.demo.controller.DemoController", method = "exception", location=@Location(Kind.ERROR) ) public static void onthrowreturn() { if (currentException != null) { // 打印异常堆栈 BTraceUtils.Threads.jstack(currentException); BTraceUtils.println("====================="); // 打印完之后就置空 currentException = null; } } }
location=@Location(Kind.ERROR)可以把吞掉的异常打印出来
-
-
3.具体的监控步骤
-
进入TestBrace所在的命令行界面,然后输入:
btrace 监控的程序的进程号 TestBrace.java
-
此时在浏览器中访问springboot项目中的test方法,输入king,然后就能在Btrace的命令行界面看到相应的输出
-
注解的使用
OnMethod
- @OnMethod 可以指定 clazz 、method、location。
- 由此组成了在什么时机(location 决定)监控某个类/某些类(clazz 决定)下的某个方法/某些方法(method 决定)。拦截时机由 location 决定,当然也可为同一个定位加入多个拦截时机,即可以在进入方法时拦截、方法返回时拦截、抛出异常时拦截
clazz
- clazz 支持,精准定位、正则表达式定位、按 接 口 或 继 承 类 定 位 < 例 如 要 匹 配 继 承 或 实 现 了 com.kite.base 的 接 口 或 基 类 的 , 只 要 在 类 前 加 上 + 号 就 可 以 了 , 例 如@OnMethod(clazz="+com.kite.base", method=“doSome”)>、按注解定位<在前面加上 @ 即可,例如@OnMethod(clazz="@javax.jws.WebService",method="@javax.jws.WebMethod")>method 支持精准定位、正则表达式定位、按注解定位
location
- 1.Kind.Entry 与 Kind.Return分别表示函数的开始和返回,不写 location 的情况下,默认为 Kind.Entry,仅获取参数值,可以用 Kind.Entry ,要获取返回值或执行时间就要用 Kind.Return
- 2.Kind.Error, Kind.Throw 和 Kind.Catch,表示异常被 throw 、异常被捕获还有异常发生但是没有被捕获的情况,在拦截函数的参数定义里注入一个Throwable 的参数,代表异常
- 3.Kind.Call 表示被监控的方法调用了哪些其他方法,Kind.Line 监测类是否执行到了设置的行数
BTrace 注解
- BTrace 注解可以分为:
- 类注解 @BTrace
- 方法注解如@OnMethod
- 参数注解如:@ProbeClassName
参数注解
- @ProbeClassName
用于标记处理方法的参数,仅用户@OnMethod, 该参数的值就是被跟踪的类名称 - @ProbeMethodName
用于表姐处理方法的参数,仅用户 @OnMethod,该参数值是被跟踪方法名称 - @Self
当前截取方法的封闭实例参数 - @Return
当前截取方法的的返回值, 只对 location=@Location(Kind.RETURN) 生效 - @Duration
当前截取方法的执行时间 - @TargetInstance
当前截取方法内部调用的实例 - @TargetMethodOrField
当前截取方法内部被调用的方法名
方法注解
- @OnMethod
用于指定跟踪方法到目标类,目标方法和目标位置
格式
@Location 属性有:
- value 默认值为 Kind.ENTRY 即参数的入口位置
- where 限定探测位置 默认值为 Where.BEFORE 也可以设置为 Where.AFTER
- clazz
- method
- field
- type
- line
@Kind 注解的值有
- Kind.ENTRY-被 trace 方法参数
- Kind.RETURN-被 trace 方法返回值
- Kind.THROW -抛异常
- Kind.ARRAY_SET, Kind.ARRAY_GET -数组索引
- Kind.CATCH -捕获异常
- Kind.FIELD_SET -属性值
- Kind.LINE -行号
- Kind.NEW -类名
- Kind.ERROR -抛异常
@OnTimer
- 用于指定跟踪操作定时执行。value 用于指定时间间隔
@OnError
- 当 trace 代码抛异常或者错误时,该注解的方法会被执行.如果同一个 trace 脚本中其他方法抛异常,该注解方法也会被执行。
Btrace 的限制
-
BTrace 最终借 Instrument 实现 class 的替换。出于安全考虑,Instrument 在使用上存在诸多的限制,这就好比给一架正在飞行的飞机换
发动机一样一样的,因此 BTrace 脚本的限制如下:- 不允许创建对象
- 不允许创建数组
- 不允许抛异常
- 不允许 catch 异常
- 不允许随意调用其他对象或者类的方法,只允许调用 com.sun.btrace.BTraceUtils 中提供的静态方法(一些数据处理和信息输出工具)
- 不允许改变类的属性
- 不允许有成员变量和方法,只允许存在 static public void 方法
- 不允许有内部类、嵌套类
- 不允许有同步方法和同步块
- 不允许有循环
- 不允许随意继承其他类(当然,java.lang.Object 除外)
- 不允许实现接口
- 不允许使用 assert
- 不允许使用 Class 对象
-
如此多的限制,其实可以理解。BTrace 要做的是,虽然修改了字节码,但是除了输出需要的信息外,对整个程序的正常运行并没有影响。
工具总结
- 其实作为 Java 的动态追踪技术,站在比较底层的角度上来说,底层无非就是基于 ASM、Java Attach API、Instrument 开发的创建。Arthas 都是针前面这些技术的一个封装而已。
- Btrace 功能虽然强大,但都是比较难入门,这就是为什么 Btrace 出来这么多年,还是只在小范围内被使用。相对来说,Arthas 显的友好而且安全的多。
- 但无论工具如何强大,一些基础知识是需要牢固掌握的,否则,工具中出现的那些术语,也会让人一头雾水。工具常变,但基础更加重要。如果你想要一个适应性更强的技术栈,还是要多花点时间在原始的排查方法上。