原标题: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 的支持。返回搜狐,查看更多

责任编辑:

java instrument_JAVA Instrument技术实战以及在QTrace中的应用相关推荐

  1. 路由交换技术实战七 FR 网络中配置 OSPF( 完成版 )

    帧中继网络用户接口上最多可支持1024条虚电路,其中用户可用的dlci范围是:16-1007 DLCI只具有局部意义,即交换机上不同的端口可以使用相同的DLCI号. 实验要求: 1.掌握配置帧中继的基 ...

  2. 《大数据架构和算法实现之路:电商系统的技术实战》——1.5 相关软件:R和Mahout...

    本节书摘来自华章计算机<大数据架构和算法实现之路:电商系统的技术实战>一书中的第1章,第1.5节,作者 黄 申,更多章节内容可以访问云栖社区"华章计算机"公众号查看. ...

  3. 《大数据架构和算法实现之路:电商系统的技术实战》——1.6 案例实践

    本节书摘来自华章计算机<大数据架构和算法实现之路:电商系统的技术实战>一书中的第1章,第1.6节,作者 黄 申,更多章节内容可以访问云栖社区"华章计算机"公众号查看. ...

  4. 《编译与反编译技术实战 》一2.3 编译器的设计与实现概述

    本节书摘来自华章出版社<编译与反编译技术实战 >一书中的第2章,第2.3节,庞建民 主编 ,刘晓楠 陶红伟 岳 峰 戴超 编著,更多章节内容可以访问云栖社区"华章计算机" ...

  5. 《编译与反编译技术实战》——第1章 实践的环境与工具 1.1 实践环境概述

    本节书摘来自华章计算机<编译与反编译技术实战>一书中的第1章,第1.1节,作者 刘晓楠 陶红伟 岳峰 戴超,更多章节内容可以访问云栖社区"华章计算机"公众号查看. 第1 ...

  6. 《编译与反编译技术实战》——2.1节编译器、解释器及其工作方式

    本节书摘来自华章社区<编译与反编译技术实战>一书中的第2章,第2.1节编译器.解释器及其工作方式,作者刘晓楠 陶红伟 岳 峰 戴超,更多章节内容可以访问云栖社区"华章社区&quo ...

  7. 《Java EE核心框架实战》—— 2.3 resultMap 标签

    本节书摘来异步社区<Java EE核心框架实战>一书中的第2章,第2.3节,作者: 高洪岩,更多章节内容可以访问云栖社区"异步社区"公众号查看. 2.3 < re ...

  8. 《编译与反编译技术实战》——第2章编译器实践概述

    本节书摘来自华章社区<编译与反编译技术实战>一书中的第2章编译器实践概述,作者刘晓楠 陶红伟 岳 峰 戴超,更多章节内容可以访问云栖社区"华章社区"公众号查看 第2章 ...

  9. 《编译与反编译技术实战》——1.2 词法分析生成器LEX

    本节书摘来自华章计算机<编译与反编译技术实战>一书中的第1章,第1.2节,作者 刘晓楠 陶红伟 岳峰 戴超,更多章节内容可以访问云栖社区"华章计算机"公众号查看. 1. ...

  10. 《大数据架构和算法实现之路:电商系统的技术实战》——2.4 案例实践

    本节书摘来自华章计算机<大数据架构和算法实现之路:电商系统的技术实战>一书中的第2章,第2.4节,作者 黄 申,更多章节内容可以访问云栖社区"华章计算机"公众号查看. ...

最新文章

  1. 前、后端分离权限控制设计和实现思路
  2. Nebula3的Input系统
  3. CSS中颜色代码和单位
  4. python中的小魔法(一)
  5. js压缩图片_Web 性能优化: 图片优化让网站大小减少 62%
  6. C#基础视频教程4.3 如何编写简单的计算器
  7. 多对多的添加修改,显示,的逻辑步骤
  8. python压缩算法_用python实现LZ78压缩算法
  9. numpy学习之创建数组
  10. Android 代码混淆、第三方平台加固加密、渠道分发 完整教程(转)
  11. Java教程01.Java简介与环境配置
  12. LINGO 18.0安装教程
  13. magisk核心功能模式是什么_科技板块——深入解析MM管理器
  14. 用户体验测试一样很重要
  15. 世界坐标系、相机坐标系、图像坐标系、像素坐标系
  16. 关键点检测——无监督
  17. 《RAFT-Stereo:Multilevel Recurrent Field Transforms for Stereo Matching》论文笔记
  18. Ceph分布式存储实战:从0搭建一个存储集群,并把块设备镜像映射到CentOS 7系统上的步骤
  19. 浙江最新通信施工安全员机考真题及答案解析
  20. 熬夜整理小米Java面试题,已拿offer

热门文章

  1. Python生成 一维条码
  2. GH4199变形合金
  3. Python量化投资——年化收益26%,一个大小盘轮轮动量化投资策略的回测效果
  4. SSD-tensorflow-1 demo
  5. GIS二次开发平台比较之我想
  6. 产品原型设计实战(一):产品设计相关工作
  7. JavaProject-IP归属地查询
  8. 我们把计算机硬件系统和软件系统称为,中国大学MOOC:\我们把计算机硬件系统和软件系统总称为( )。\;...
  9. 全站仪数据导入电脑_南方全站仪怎么连接电脑传输数据
  10. 8个免费在线字体转换器