原标题:JAVA Instrument技术实战以及在QTrace中的应用
张楚宸,Java开发工程师,2016年加入去哪儿网。接触过Android、RN、Angular等大前端技术。现阶段主要从事数据算法工程相关工作。
引文
本系列分为两篇文章,主要分为三个板块: 1.介绍了 Instrument 相关技术名词以及概念; 2.Instrument 代码实战,抛弃 SpringAOP 实现 AOP 方法; 3.介绍 Instrument 在 Qunar 的全链路追踪 QTrace 的 Client 端的应用。 通过该系列可以了解到 Instrument 技术以及如何应用。 在第一篇文章中,主要包含以下内容: Instrument、Attach API、JVMTI、Agent、ASM 等概念; 代码实战,实现通过 ASM 修改字节码打印方法耗时。 本文是该系列的第二篇,主要包含以下内容: 代码实战,在 ASM 修改字节码的基础上实现真正的 AOP; 通过 QTrace 中部分代码分析,了解 QTrace 是如何实现代码插桩。
抛开Spring实现AOP打印方法耗时阶段二:加入Java Agent Instrument
在上一篇代码实战的阶段一中我们实现了修改字节码文件。但是存在一个问题,只有在 run 过一次后才能修改字节码,并且重新编译会覆盖,这和我们想要的功能其实还差了很多,我们希望随应用启动就可以完成修改字节码。
定义agent入口,在其中暴露出Instrumentation实例,供之后使用。
public class AgentMain {
private static Instrumentation inst;
/** 命令行启动 */
public static void premain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agent premain");
inst = instrumentation;
}
/** 类加载调用 */
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
System.out.println("agent agentmain");
inst = instrumentation;
}
public static Instrumentation instrumentation() {
return inst;
}
}
修改 pom 中的 Maven 配置,在打包后生成的 META-INF/MANIFEST.MF 中指定 Agent 入口信息。
org.apache.maven.plugins
maven-jar-plugin
2.6
asm.instrument.AgentMain
asm.instrument.AgentMain
true
true
生成的文件如下:
Manifest-Version: 1.0
Premain-Class: asm.instrument.AgentMain
Archiver-Version: Plexus Archiver
Built-By: zcc
Agent-Class: asm.instrument.AgentMain
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Created-By: Apache Maven 3.0.5
Build-Jdk: 1.8.0_144
实现Agent加载器
主要通过 jdktools.jar中Attach API 实现; 不同平台对于 com.sun.tools.attach.VirtualMachine 有不同的实现,这里环境为 linux 所以仅使用了 sun.tools.attach.LinuxVirtualMachine。
/**
* JDKAgentLoader
*
* @date 18-11-15 下午3:04
*/
public class JDKAgentLoader {
private static final AttachProvider ATTACH_PROVIDER = new AttachProvider() {
@Override
public String name() {
return null;
}
@Override
public String type() {
return null;
}
@Override
public VirtualMachine attachVirtualMachine(String id) {
return null;
}
@Override
public List listVirtualMachines() {
return null;
}
};
private final String jarFilePath;
JDKAgentLoader(String jarFilePath) {
this.jarFilePath = jarFilePath;
}
void loadAgent() {
VirtualMachine vm;
if (AttachProvider.providers().isEmpty()) {
String vmName = System.getProperty("java.vm.name");
if (vmName.contains("HotSpot")) {
vm = getVirtualMachineImplementationFromEmbeddedOnes();
} else {
String helpMessage = getHelpMessageForNonHotSpotVM(vmName);
throw new IllegalStateException(helpMessage);
}
} else {
vm = attachToRunningVM();
}
loadAgentAndDetachFromRunningVM(vm);
}
private static VirtualMachine getVirtualMachineImplementationFromEmbeddedOnes() {
Class extends VirtualMachine> vmClass = findVirtualMachineClassAccordingToOS();
Class>[] parameterTypes = {AttachProvider.class, String.class};
String pid = getProcessIdForRunningVM();
try {
// This is only done with Reflection to avoid the JVM pre-loading all the XyzVirtualMachine classes.
Constructor extends VirtualMachine> vmConstructor = vmClass.getConstructor(parameterTypes);
return vmConstructor.newInstance(ATTACH_PROVIDER, pid);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private static Class extends VirtualMachine> findVirtualMachineClassAccordingToOS() {
String osName = System.getProperty("os.name");
if (osName.startsWith("Linux") || osName.startsWith("LINUX")) {
return LinuxVirtualMachine.class;
}
throw new IllegalStateException("Cannot use Attach API on unknown OS: " + osName);
}
private static String getProcessIdForRunningVM() {
String nameOfRunningVM = ManagementFactory.getRuntimeMXBean().getName();
int p = nameOfRunningVM.indexOf('@');
return nameOfRunningVM.substring(0, p);
}
private String getHelpMessageForNonHotSpotVM(String vmName) {
String helpMessage = "To run on " + vmName;
if (vmName.contains("J9")) {
helpMessage += ", add /lib/tools.jar to the runtime classpath (before jmockit), or";
}
return helpMessage + " use -javaagent:" + jarFilePath;
}
private static VirtualMachine attachToRunningVM() {
String pid = getProcessIdForRunningVM();
try {
return VirtualMachine.attach(pid);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void loadAgentAndDetachFromRunningVM(VirtualMachine vm) {
try {
vm.loadAgent(jarFilePath, null);
vm.detach();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}
Instrument加载入口
LoadAgent 方法为 Agent 入口调用上面的 Agent 加载器完成加载 Agent。 Instrumentation 方法通过反射拿到 AgentMain 暴露出的 Instrument 实例。
public class Instruments {
private static final Logger logger = LoggerFactory.getLogger(Instruments.class);
private static final int LOADED_SUCCESS = 1;
private static final int LOADED_FAILED = -1;
private static final int N = 0;
private static int loaded = N;
public synchronized boolean loadAgent() {
if (loaded == LOADED_SUCCESS) return true;
if (loaded == LOADED_FAILED) return false;
try {
JDKAgentLoader loader = new JDKAgentLoader(getAgentPath());
loader.loadAgent();
loaded = LOADED_SUCCESS;
} catch (Throwable e) {
logger.warn("无法加载插桩agent,字节码插桩不开启");
logger.debug("该条日志是DEBUG级别日志,见到此异常请忽略,谢谢", e);
loaded = LOADED_FAILED;
}
return loaded == LOADED_SUCCESS;
}
private String getAgentPath() {
ProtectionDomain domain = AgentMain.class.getProtectionDomain();
CodeSource source = domain.getCodeSource();
return new File(source.getLocation().getPath()).getAbsolutePath();
}
public Instrumentation instrumentation() {
ClassLoader mainAppLoader = ClassLoader.getSystemClassLoader();
try {
final Class> javaAgentClass = mainAppLoader.loadClass(AgentMain.class.getCanonicalName());
final Method method = javaAgentClass.getDeclaredMethod("instrumentation", new Class[0]);
return (Instrumentation) method.invoke(null, new Object[0]);
} catch (Throwable e) {
logger.error("can not get agent class", e);
return null;
}
}
}
实现类转换器,在类加载时修改内存中的字节码
修改字节码还是使用阶段一中就已经实现好的 AopClassVisitor。
public final class MethodCostTimeFileTransformer implements ClassFileTransformer {
private List aopClassFileList;
public MethodCostTimeFileTransformer(List aopClassFileList) {
this.aopClassFileList = aopClassFileList;
}
private byte[] transform(String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
if (!aopClassFileList.contains(className)) {
return null;
}
ClassReader classReader = new ClassReader(classfileBuffer);
ClassWriter tClassWrite = new ClassWriter(COMPUTE_MAXS);
AopClassVisitor tAopClassVisitor = new AopClassVisitor("", ASM5, tClassWrite);
classReader.accept(tAopClassVisitor, EXPAND_FRAMES);
return tClassWrite.toByteArray();
}
@Override
public byte[] transform(ClassLoader loader, String className, Class> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
try {
return transform(className, classBeingRedefined, protectionDomain, classfileBuffer);
} catch (Throwable e) {
System.err.print("Class: ");
System.err.print(className);
System.err.print(", ClassLoader: ");
System.err.print(loader);
System.err.print(" transform failed.n");
e.printStackTrace(System.err);
return null;
}
}
}
在启动时将类转换器插桩
istruments.addTransformer(new MethodCostTimeFileTransformer(classPathList));
阶段三 使用注解执行器,获取注解信息
阶段二虽然实现在了在内存中修改字节码,但是仍然存在不足。 阶段一中通过注解可以拿到所有需要修改的类和方法,但是在阶段二中如果通过在运行时看注解的方式来做的话,我们需要修改的类就会在插桩之前就已经被 JVM 加载完成。 我们希望可以在类加载之前就知道有哪些方法需要打印耗时。
注解执行器
注解执行器是在编译期执行; 我们在编译期将注解标注的类和方法信息写入文件,在运行时通过读取文件避免了加载对应的类来实现我们的目的。
@SupportedAnnotationTypes("asm.annotation.Cost")
public class CostAnnotationProcessor extends AbstractProcessor {
public static final String FILE_NAME = "cost_annotation";
@Override
public boolean process(Set extends TypeElement> annotations, RoundEnvironment roundEnv) {
String packageName = "asm";
List classList = PackageUtil.getHasCostAnnotationClassList(roundEnv.getElementsAnnotatedWith(QTrace.class));
System.out.println(classList.toArray());
FileObject resource = null;
Writer writer = null;
try {
resource = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", FILE_NAME);
writer = resource.openWriter();
writer.write(JsonUtil.toString(classList));
} catch (IOException e) {
e.printStackTrace();
} finally {
if (writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return false;
}
}
Instrument在QTrace中的应用
在 Qunar,Instrument 在全链路追踪神器 QTrace 中应用较多,通过插桩的方式修改字节码在应用内和应用间传递链路上下文达到追踪的目的。 QTrace 的实现和上述中实现 AOP 打印方法耗时的过程基本相同。
Maven:qunar.tc.qtracer:qtracer-instrument-annotation:1.3.9
声明了三个注解,如: qunar.tc.qtracer.annotation.QTrace,用来标识需要插桩处理的代码。
Maven:qunar.tc.qtracer:qtracer-instrument-asm:1.3.9
asm 包的 copy 内容。
Maven:qunar.tc.qtracer:qtracer-instrument-annotation-processor:1.3.9
将QTrace信息写入文件,后续在agent启动时拿到该文件中信息作为需要插桩的代码。 qunar.tc.qtracer.instrument.annotation.processor.QTraceAnnotationProcessor 继承 javax. annotation. processing. AbstractProcessor 在编译期提前处理QTrace 注解。META-INF/services/javax.annotation.processing.Processor 中配置 processor。生成文件 META-INF/qtracer-annotation,用来记录已经添加QTrace注解的类和方法,以供后续拿到该信息。
文件内容如下所示:
xxxPackage.xxxClass:xxxMethod(java.lang.StringdepAirport,java.lang.StringarrAirport,java.lang.StringdepDate):type=QTRACE:
Maven:qunar.tc.qtracer:qtracer-instrument-agent:1.3.9
Java Agent 入口并暴露 Instrument 实例。
/**
* JavaAgent入口。
* 详情参考Package Document中的章节"Starting Agents After VM Startup"
*
* User: zhaohuiyu
* Date: 8/30/12
* Time: 12:33 PM
*/
public class AgentMain {
private static Instrumentation inst;
/** 命令行启动 */
public static void premain(String agentArgs, Instrumentation instrumentation) {
inst = instrumentation;
}
/** 类加载调用 */
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
inst = instrumentation;
}
public static Instrumentation instrumentation() {
return inst;
}
}
Manifest-Version: 1.0
Built-By: jenkins
Build-Jdk: 1.7.0_40
Agent-Class: qunar.tc.qtracer.instrument.AgentMain
Premain-Class: qunar.tc.qtracer.instrument.AgentMain
Created-By: Apache Maven 3.0.5
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Archiver-Version: Plexus Archiver
Maven:qunar.tc.qtracer:qtracer-instrument-tools:1.3.9
JDK 自带的 tools.jar 中 sun.tools 包下关于虚拟机和 Attach 相关类的 copy。 将不同平台的 com.sun.tools.attach.VirtualMachine 都 copy 到一起作为公共类。
Maven:qunar.tc.qtracer:qtracer-instrument-lib:1.3.9
核心代码 qunar. ServletWatcher#init(int, javax.servlet.ServletContext) 中调用了 qunar.tc.qtracer.instrument.Instruments#start 启动入口。
public void start() {
if (!inited.compareAndSet(false, true)) return;
Configuration configuration = new Configuration();
if (!configuration.isInstrument()) {
logger.warn("未开启instrument");
return;
}
JavaAgentLoader loader = new JavaAgentLoader();
boolean agentLoaded = loader.loadAgent();
if (!agentLoaded) {
logger.warn("agent load failed");
return;
}
Instrumentation inst = instrumentation();
if (inst == null) {
logger.warn("can not get instrumentation");
return;
}
try {
inst.addTransformer(new QTraceClassFileTransformer(configuration));
} catch (Throwable ignore) {
logger.error("add class transformer failed");
return;
}
logger.info("开启字节码插桩");
ServerManagement management = ServiceFinder.getService(ServerManagement.class);
management.addRequestHandler("/_/qtracer", new InstrumentHandler(configuration, inst));
management.addRequestHandler("/_/jvm_profile", new JvmProfilingHandler());
}
new Configuration() 读取配置 META-INF/qtracer-annotation。按照该配置来判断哪些类和方法需要处理 QTrace。 qunar.tc.qtracer.instrument.JavaAgentLoader#loadAgent 加载 Agent。
private void loadAgentAndDetachFromRunningVM(VirtualMachine vm) {
try {
vm.loadAgent(jarFilePath, null);
vm.detach();
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
qunar.tc.qtracer.instrument.Instruments#instrumentation 反射拿到 Agent 中的 Instrument 实例。 inst.addTransformer(new QTrace Class File Transformer(configuration)); 开启插桩。
TraceClassVisitor&TraceMethodVisitor 功能注释及主要流程
/**
* 完成底层字节码替换。
*
* 实现:
*
* QTraceScope scope = QTraceClientGetter.getClient().startTrace(content);
* scope.addAnnotation(Constants.QTRACE_TYPE, type);
* try {
* source(XXX, XXX, XXX);
* } catch (RuntimeException e) {
* scope.addAnnotation(Constants.EXCEPTION_KEY, "type:" + e.getClass() + ",message:" + e.getMessage());
* scope.addAnnotation(Constants.QTRACE_STATUS, Constants.QTRACE_STATUS_ERROR);
* throw e;
* } finally {
* scope.close();
* }
*
*
*
* 1.存在注解的方法会进行替换,没有添加注解的方法继续使用原来的字节码。
* 2.按照方法声明的异常个数追加catch块,没有声明异常,则默认使用RuntimeException作为catch块。
* 3.追加的新的方法为targetMethodName = "$" + sourceMethodName + "$qtrace$annotation$",sourceMethodName为原方法名。
* 4.新方法私有(private)。
* 5.唯一性参照方法名 + 参数类型 + 返回类型。
* 6.构造方法和静态构造方法忽略替换。
* 7.interface、abstract方法、navicat方法忽略替换。
* 8.同级方法替换可以支持private方法、final方法、this调用等cglib无法支持的场景。
*
*
* @author Daniel Li
* @since 29 March 2015
*/
/**
* 将原来的方法重命名为一个private方法,然后在原来未改名的方法里调用原来的方法
*/
@Override
public void visitCode() {
super.visitCode();
traceMethod.visitCode();
int scopeVarIndex = startOfVarIndex + totalParameterSize;
Label startOfTryCatch = new Label();
Label endOfTryCatch = new Label();
Label[] exceptionHandlers = new Label[newExceptionsLen];
//catchs
for (int i = 0, length = exceptionHandlers.length; i < length; i++) {
traceMethod.visitTryCatchBlock(startOfTryCatch, endOfTryCatch, exceptionHandlers[i] = new Label(), newMethodExceptions[i]);
}
//finally
Label endOfFinally = new Label();
Label handlerOfFinally = new Label();
traceMethod.visitTryCatchBlock(startOfTryCatch, endOfTryCatch, handlerOfFinally, null);
traceMethod.visitTryCatchBlock(exceptionHandlers[0], endOfFinally, handlerOfFinally, null);
startTrace(scopeVarIndex);
attachWatchId(scopeVarIndex);
attachFields(scopeVarIndex);
attachArgs(scopeVarIndex);
int returnVarIndex = scopeVarIndex + 1;
//try{
//call original method
//}catch(...){
traceMethod.visitLabel(startOfTryCatch);
callOriginal(returnVarIndex, startOfVarIndex, traceMethod);
traceMethod.visitLabel(endOfTryCatch);
attachReturnValue(scopeVarIndex, returnVarIndex);
endTrace(scopeVarIndex);
Label end = null;
if (!hasReturn) {
end = new Label();
traceMethod.visitJumpInsn(GOTO, end);
} else {
traceMethod.visitVarInsn(returnType.getOpcode(ILOAD), returnVarIndex);
traceMethod.visitInsn(returnType.getOpcode(IRETURN));
}
emitCatchBlocks(scopeVarIndex, exceptionHandlers);
traceMethod.visitLabel(handlerOfFinally);
traceMethod.visitVarInsn(ASTORE, returnVarIndex);
//finally
traceMethod.visitLabel(endOfFinally);
endTrace(scopeVarIndex);
traceMethod.visitVarInsn(ALOAD, returnVarIndex);
traceMethod.visitInsn(ATHROW);
if (!hasReturn) {
traceMethod.visitLabel(end);
traceMethod.visitInsn(RETURN);
}
}
Maven:qunar.tc.qtracer:qtracer-common:1.3.9
QTrace 公共包,维护常量以及提供基础支持。
Maven:qunar.tc.qtracer:qtracer-client:1.3.9
提供 QTrace Client 端功能。 包括全链路追踪在 Client 端的追踪、收集等功能。
Maven:qunar.tc.qtracer:qtracer-instrument-http:1.3.9
提供 QTrace 对 Servlet 的支持。返回搜狐,查看更多
责任编辑: