开篇
JVM Profile的方法采集通过修改字节码在原来方法体的前置和后置增加采集耗时的代码。核心是基于基于java自带的instrument包和javassist包来实现的。
整个核心逻辑如下:
- 得到用户传入需要拦截的方法列表。
- 在方法前后织入前置和后置耗时统计代码。
- 内部保存耗时然后上报耗时。
源码分析
- durationProfilingFilter 保存需要采集耗时的方法列表。
- argumentFilterProfilingFilter 保存需要采集方法参数的方法列表。
public class JavaAgentFileTransformer implements ClassFileTransformer {
private static final AgentLogger logger = AgentLogger.getLogger(JavaAgentFileTransformer.class.getName());
private ClassAndMethodFilter durationProfilingFilter;
private ClassMethodArgumentFilter argumentFilterProfilingFilter;
/**
*
* @param durationProfiling 是采集的类和接口封装的对象
* @param argumentProfiling 是
*/
public JavaAgentFileTransformer(List durationProfiling, List argumentProfiling) {
this.durationProfilingFilter = new ClassAndMethodFilter(durationProfiling);
this.argumentFilterProfilingFilter = new ClassMethodArgumentFilter(argumentProfiling);
}
}
转码过程
- 获取类的字节码并解析类的method。
- 判断method是否在采集列表当中,如果不在采集列表就直接返回。
- 针对所有需要被采集耗时的方法通过transformMethod进行字节码修改。
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
// 用于对原有的类进行转换并返回新的字节码
if (className == null || className.isEmpty()) {
logger.debug("Hit null or empty class name");
return null;
}
return transformImpl(loader, className, classfileBuffer);
} catch (Throwable ex) {
logger.warn("Failed to transform class " + className, ex);
return classfileBuffer;
}
}
private byte[] transformImpl(ClassLoader loader, String className, byte[] classfileBuffer) {
// 判断监控列表是否为空
if (durationProfilingFilter.isEmpty()
&& argumentFilterProfilingFilter.isEmpty()) {
return null;
}
// 转换类名替换"/"为"."
String normalizedClassName = className.replaceAll("/", ".");
logger.debug("Checking class for transform: " + normalizedClassName);
// 判断是否在监控列表当中
if (!durationProfilingFilter.matchClass(normalizedClassName)
&& !argumentFilterProfilingFilter.matchClass(normalizedClassName)) {
return null;
}
byte[] byteCode;
logger.info("Transforming class: " + normalizedClassName);
try {
// ClassPool首先负责加载该类然后判断该类的方法是否在拦截列表当中
ClassPool classPool = new ClassPool();
classPool.appendClassPath(new LoaderClassPath(loader));
final CtClass ctClass;
try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(classfileBuffer)) {
ctClass = classPool.makeClass(byteArrayInputStream);
}
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
for (CtMethod ctMethod : ctMethods) {
boolean enableDurationProfiling = durationProfilingFilter.matchMethod(ctClass.getName(), ctMethod.getName());
List enableArgumentProfiler = argumentFilterProfilingFilter.matchMethod(ctClass.getName(), ctMethod.getName());
// 修改方法的字节码
transformMethod(normalizedClassName, ctMethod, enableDurationProfiling, enableArgumentProfiler);
}
// 返回类的二进制编码
byteCode = ctClass.toBytecode();
// ClassPool中删除该类
ctClass.detach();
} catch (Throwable ex) {
ex.printStackTrace();
logger.warn("Failed to transform class: " + normalizedClassName, ex);
byteCode = null;
}
return byteCode;
}
方法增加前后置采集代码
- 通过method.insertBefore()在原有的方法体前置添加耗时采集代码。
- 通过method.insertAfter()在原有的方法体后置添加耗时采集代码
// 对类进行前后拦截
private void transformMethod(String normalizedClassName, CtMethod method, boolean enableDurationProfiling, List argumentsForProfile) {
if (method.isEmpty()) {
logger.info("Ignored empty class method: " + method.getLongName());
return;
}
if (!enableDurationProfiling && argumentsForProfile.isEmpty()) {
return;
}
try {
// 在方法中增加局部计时变量 startMillis_java_agent_instrument 和 durationMillis_java_agent_instrument
if (enableDurationProfiling) {
method.addLocalVariable("startMillis_java_agent_instrument", CtClass.longType);
method.addLocalVariable("durationMillis_java_agent_instrument", CtClass.longType);
}
// 添加前置拦截代码
StringBuilder sb = new StringBuilder();
// 代码块的开始标志 "{"
sb.append("{");
// 添加开始采集程序执行开始时间的代码
if (enableDurationProfiling) {
sb.append("startMillis_java_agent_instrument = System.currentTimeMillis();");
}
// 如果该方法的某个参数需要被采集,添加采集代码
for (Integer argument : argumentsForProfile) {
if (argument >= 1) {
// 在javassist当中$1表示执行中第一个参数的值,这里根据传进来的下标argument来标识监控那个具体参数
// collectMethodArgument负责对接口参数的值进行监控
sb.append(String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodArgument(\"%s\", \"%s\", %s, String.valueOf($%s));}catch(Throwable ex){ex.printStackTrace();}",
normalizedClassName,
method.getName(),
argument,
argument));
} else {
// 如果等于0那么采用其他方法进行采集
sb.append(String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodArgument(\"%s\", \"%s\", %s, \"\");}catch(Throwable ex){ex.printStackTrace();}",
normalizedClassName,
method.getName(),
argument,
argument));
}
}
// 添加代码块结尾
sb.append("}");
// 在原有方法体之前插入前置代码块
method.insertBefore(sb.toString());
if (enableDurationProfiling) {
// 在原有方法体之后插入后置代码块
// collectMethodDuration负责采集接口耗时
method.insertAfter("{" +
"durationMillis_java_agent_instrument = System.currentTimeMillis() - startMillis_java_agent_instrument;" +
String.format("try{com.uber.profiling.transformers.MethodProfilerStaticProxy.collectMethodDuration(\"%s\", \"%s\", durationMillis_java_agent_instrument);}catch(Throwable ex){ex.printStackTrace();}", normalizedClassName, method.getName()) +
// "System.out.println(\"Method Executed in ms: \" + durationMillis);" +
"}");
}
// 整个函数体的执行代码增加了前置和后置处理,实现了耗时的监控
logger.info("Transformed class method: " + method.getLongName() + ", durationProfiling: " + enableDurationProfiling + ", argumentProfiling: " + argumentsForProfile);
} catch (Throwable ex) {
ex.printStackTrace();
logger.warn("Failed to transform class method: " + method.getLongName(), ex);
}
}
}
采集结果
{
"metricName": "duration.min",
"processName": "[email protected]",
"appId": null,
"host": "xiaozhideMacBook-Pro.local",
"processUuid": "1e580f6e-0493-4e5b-bee2-a61c5f7b097d",
"metricValue": 894.0,
"methodName": "publicSleepMethod",
"className": "com.uber.profiling.examples.HelloWorldApplication",
"epochMillis": 1536072801037,
"tag": "mytag"
}