动态追踪技术底层分析
什么是动态追踪
- 不用关闭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.打包
- 在\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文件
实战演练
- 下载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 显的友好而且安全的多。
- 但无论工具如何强大,一些基础知识是需要牢固掌握的,否则,工具中出现的那些术语,也会让人一头雾水。工具常变,但基础更加重要。如果你想要一个适应性更强的技术栈,还是要多花点时间在原始的排查方法上。