java探针利用了javaAgent + ASM字节码注入工具实现了动态修改类文件的功能。像skywalking和arthas都使用到了这个技术。
具体原理为:

jdk1.5以后引入了javaAgent技术,javaAgent是运行方法之前的拦截器。我们利用javaAgent和ASM字节码技术,在JVM加载class二进制文件的时候,利用ASM动态的修改加载的class文件,在监控的方法前后添加计时器功能,用于计算监控方法耗时,同时将方法耗时及内部调用情况放入处理器,处理器利用栈先进后出的特点对方法调用先后顺序做处理,当一个请求处理结束后,将耗时方法轨迹和入参map输出到文件中,然后根据map中相应参数或耗时方法轨迹中的关键代码区分出我们要抓取的耗时业务。最后将相应耗时轨迹文件取下来,转化为xml格式并进行解析,通过浏览器将代码分层结构展示出来,方便耗时分析。

上篇我们介绍了JavaAgent的基本使用,下面介绍如何去动态的修改类的字节码文件,这个才是agent实现更强大功能的核心所在!

Instrumentation接口

Instrumentation接口位于jdk1.6包java.lang.instrument包下,Instrumentation指的是可以独立于应用程序之外的代理程序,可以用来监控和扩展JVM上运行的应用程序,相当于是JVM层面的AOP。

功能:
监控和扩展JVM上的运行程序,它可以替换和修改java类的字节码以便采集数据,用于监控,性能统计,覆盖率分析,事件记录等。可以用在程序启动时,也可以用于程序运行时动态attach。

比如说一个Java程序在JVM上运行,这时如果需要监控JVM的状态,除了使用JDK自带的jps等命令之外,就可以通过instrument来更直观的获取JVM的运行情况;
或者一个Java方法在JVM中执行,如果我想获取这个方法的执行时间又不想改代码,常用的做法是通过Spring的AOP来实现,而AOP通过面向切面编程,而instrument是在JVM层面上直接改动java方法来实现。

public interface Instrumentation{//添加ClassFileTransformervoid addTransformer(ClassFileTransformer transformer, boolean canRetransform);//添加ClassFileTransformervoid addTransformer(ClassFileTransformer transformer);//移除ClassFileTransformerboolean removeTransformer(ClassFileTransformer transformer);//是否可以被重新定义boolean isRetransformClassesSupported();//重新定义Class文件void redefineClasses(ClassDefinition... definitions)throws ClassNotFoundException, UnmodifiableClassException;//是否可以修改Class文件boolean isModifiableClass(Class<?> theClass);//获取所有加载的Class@SuppressWarnings("rawtypes")Class[] getAllLoadedClasses();//获取指定类加载器已经初始化的类@SuppressWarnings("rawtypes")Class[] getInitiatedClasses(ClassLoader loader);//获取某个对象的大小long getObjectSize(Object objectToSize);//添加指定jar包到启动类加载器检索路径void appendToBootstrapClassLoaderSearch(JarFile jarfile);//添加指定jar包到系统类加载检索路径void appendToSystemClassLoaderSearch(JarFile jarfile);//本地方法是否支持前缀boolean isNativeMethodPrefixSupported();//设置本地方法前缀,一般用于按前缀做匹配操作void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix);
}

要是定义了操作java类的class文件方法,这里又涉及到了ClassFileTransformer接口,这个接口的作用是改变Class文件的字节码,返回新的字节码数组,源码如下:

public interface ClassFileTransformer{byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException;
}

ClassFileTransformer接口只有一个方法,就是改变指定类的Class文件,该接口没有默认实现,很显然如果需要改变Class文件的内容,需要改成什么样需要使用者自己来实现。
如:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod;import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;public class MyTransformer implements ClassFileTransformer {final static String prefix = "\nlong startTime = System.currentTimeMillis();\n";final static String postfix = "\nlong endTime = System.currentTimeMillis();\n";// 被处理的方法列表final static Map<String, List<String>> methodMap = new HashMap<>();public MyTransformer() {add("com.jun.sail.myservice.service.HelloService.say");add("com.jun.sail.myservice.service.HelloService.say2");}private void add(String methodString) {String className = methodString.substring(0, methodString.lastIndexOf("."));String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);List<String> list = methodMap.computeIfAbsent(className, k -> new ArrayList<>());list.add(methodName);}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) {className = className.replace("/", ".");if (methodMap.containsKey(className)) { // 判断加载的class的包路径是不是需要监控的类CtClass ctclass = null;try {// 使用全称,用于取得字节码类<使用javassist>ctclass = ClassPool.getDefault().get(className);for (String methodName : methodMap.get(className)) {String outputStr = "\nSystem.out.println(\"this method [" + methodName+ "] cost:\" +(endTime - startTime) +\"ms.\");";// 得到这方法实例CtMethod ctmethod = ctclass.getDeclaredMethod(methodName);// 根据原来的方法 创建新的方法,名字为原来的methodNameCtMethod newMethod = CtNewMethod.copy(ctmethod, methodName, ctclass, null);// 把旧方法名字改掉,否则会冲突String oldMethodName = methodName + "$old";ctmethod.setName(oldMethodName);// 构建新的方法体StringBuilder bodyStr = new StringBuilder();bodyStr.append("{");bodyStr.append(prefix);bodyStr.append(oldMethodName).append("($$);\n");// 调用原有代码,类似于method();($$)表示所有的参数bodyStr.append(postfix);bodyStr.append(outputStr);bodyStr.append("}");newMethod.setBody(bodyStr.toString());newMethod.setName(methodName);ctclass.addMethod(newMethod);}return ctclass.toBytecode();} catch (Exception e) {System.out.println("AAAAA" + e.getMessage());e.printStackTrace();}}return null;}
}

然后在permain或agentmain方法中inst.addTransformer(new MyTransformer());,其他步骤同之前,不再赘述。

Instrumentation接口相当于一个代理,当执行premain方法时,通过Instrumentation提供的API可以动态的添加管理JVM加载的Class文件,Instrumentation管理着ClassFileTransformer。
ClassFileTransformer接口可以动态的改变Class文件的字节码,在加载字节码的时候可以将字节码进行动态修改,具体实现需要自定义实现类来实现ClassFileTransformer接口

Java字节码生成框架大致有ASM、Javassist和byte buddy三种

  • ASM框架介绍及使用
    ASM是一种Java字节码操控框架,能够以二进制形式修改已有的类或是生成类,ASM可以直接生成二进制class文件也可以在类被加载入JVM之前动态改变类,只不过ASM在创建class字节码时说底层JVM的汇编指令,需要使用者对class组织结构和JVM汇编指令有一定的了解。由于Java 类存储在.class文件中,这些类文件中包含有:类名称、方法、属性及字节码,ASM从类文件中读入信息后改变类行为、分析类信息或者直接创建新的类。

    著名的使用到ASM的案例便是lambda表达式、CGLIB动态代理类

    ASM框架核心类包含
    ClassReader:该类用来解析编译过的class字节码文件
    ClassWriter:该类用来重新构建编译后的类,比如修改类名、属性、方法或者根据要求创建新的字节码文件
    ClassAdapter:实现了ClassVisitor接口,将对它的方法调用委托给另一个ClassVisitor对象

  • Javassist及使用
    Javassit相比于ASM要简单点,Javassit提供了更高级的API,当时执行效率上比ASM要差,因为ASM上直接操作的字节码。功能和JDK自带的反射功能类似,但是比反射要强大。

    Javassist核心类包括ClassPool:
    一个基于HashMap实现的CtClass对象容器,key上类名,value上这个类的CtClass对象
    CtClass:表示一个类,可以从ClassPool中获取
    CtMethods:表示一个类的方法
    CtFields:表示类中的属性

  • Byte Buddy及使用
    byte buddy是一个提供了API用于生成任意Java类工具包,可以生成和修改字节码。

3. Instrumentation的实现原理

说起Instrumentation的原理,就不得不先提起JVMTI:
JVMTI官网文档
JVMTI
JVMTI 是JVM Tool Interface 的缩写,是 JVM 暴露出来给用户扩展使用的接口集合,JVMTI 是基于事件驱动的,JVM每执行一定的逻辑就会调用一些事件的回调接口,这些接口可以给用户自行扩展来实现自己的逻辑。JVMTI是实现 Debugger、Profiler、Monitor、Thread Analyser 和coverage analysis等工具的统一基础,在主流 Java 虚拟机中都有实现。

JVMTIAgent
JVMTI 是一套本地代码接口,因此使用 JVMTI 需要我们与 C/C++ 以及 JNI 打交道。事实上,开发时一般采用建立一个 Agent 的方式来使用 JVMTI,它使用 JVMTI 函数,设置一些回调函数,并从 Java 虚拟机中得到当前的运行态信息,并作出自己的判断,最后还可能操作虚拟机的运行态。把 Agent 编译成一个动态链接库之后,我们就可以在 Java 程序启动的时候来加载它(启动加载模式)
主要有三个函数:

  • Agent_OnLoad方法:如果agent是在启动时加载的,那么在JVM启动过程中会执行这个agent里的Agent_OnLoad函数
  • Agent_OnAttach方法:如果agent不是在启动时加载的,而是attach到目标程序上,然后通过load命令来加载agent,由ClassFileLoadHook event提供回调,调用Agent_OnAttach方法
  • Agent_OnUnload方法:在agent卸载时调用

回到主题,Instrument 就是一种 JVMTIAgent,它实现了Agent_OnLoad和Agent_OnAttach两个方法,也就是在使用时,Instrument既可以在启动时加载,也可以在运行时动态加载

  • 启动时加载就是在启动时添加JVM参数:-javaagent:XXXAgent.jar的方式
  • 运行时加载是通过JVM的attach机制来实现,通过发送load命令来加载,这种方式明显更加灵活,对监控目标启动也无限制,arthas的attach就是基于此
private void attachAgent(Configure configure) throws Exception {VirtualMachineDescriptor virtualMachineDescriptor = null;for (VirtualMachineDescriptor descriptor : VirtualMachine.list()) {String pid = descriptor.id();if (pid.equals(Integer.toString(configure.getJavaPid()))) {virtualMachineDescriptor = descriptor;}}VirtualMachine virtualMachine = null;try {if (null == virtualMachineDescriptor) { // 使用 attach(String pid) 这种方式virtualMachine = VirtualMachine.attach("" + configure.getJavaPid());} else {virtualMachine = VirtualMachine.attach(virtualMachineDescriptor);}Properties targetSystemProperties = virtualMachine.getSystemProperties();String targetJavaVersion = targetSystemProperties.getProperty("java.specification.version");String currentJavaVersion = System.getProperty("java.specification.version");if (targetJavaVersion != null && currentJavaVersion != null) {if (!targetJavaVersion.equals(currentJavaVersion)) {AnsiLog.warn("Current VM java version: {} do not match target VM java version: {}, attach may fail.",currentJavaVersion, targetJavaVersion);AnsiLog.warn("Target VM JAVA_HOME is {}, try to set the same JAVA_HOME.",targetSystemProperties.getProperty("java.home"));}}virtualMachine.loadAgent(configure.getArthasAgent(),configure.getArthasCore() + ";" + configure.toString());} finally {if (null != virtualMachine) {virtualMachine.detach();}}}

通过 VirtualMachine , 可以attach到当前指定的jvm pid上,然后 virtualMachine.loadAgent()将编写好的agent用于监控目标。

总结:

  1. Instrumentation相当于一个JVM级别的AOP

  2. Instrumentation在JVM启动的时候监听事件,如类加载事件,JVM触发来指定的事件通过回调通知,并创建一个 Instrumentation接口的实例,然后找到MANIFEST.MF中配置的实现了premain方法的Class,然后将Instrumentation实例传入premain方法中

  3. premain方法会在main方法之前执行,可以添加ClassFileTransfer来实现对Class文件字节码的动态修改(并不会修改Class文件中的字节码,而是修改已经被JVM加载的字节码)

  4. 修改字节码的技术可以使用开源的 ASM、javassist、byteBuddy等

https://blog.csdn.net/u010862794/article/details/87773434

java探针之修改类字节码文件相关推荐

  1. [Java安全]—动态加载字节码文件

    ClassLoader加载远程字节码 POC import java.io.IOException; import java.net.URL; import java.net.URLClassLoad ...

  2. 使用JBE修改.class字节码文件

    1.场景 在excel导出页面数据的时候,发现最多只能导出5000条,但是客户方要求至少得导出66500条吧,本以为问题很简单,我并且还找到了那个类,直接一改,就行了:但是当我测试得时候发现,改动没有 ...

  3. java编译会产生多少个类文件,编译一个定义了三个类和四个方法的Java源程序文件,总共会产生多少个字节码文件 ? ( )...

    编译一个定义了三个类和四个方法的Java源程序文件,总共会产生多少个字节码文件 ? ( ) 更多相关问题 论述风化作用基本概念及其主要类型. 什么是药用植物 学?其 研究任务是什么 ? 庐山瀑布很有名 ...

  4. 浅谈Java反射机制 之 获取类的字节码文件 Class.forName(全路径名) 、getClass()、class...

    先贴上Java反射机制的概念: AVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法: 对于任意一个对象,都能够调用它的任意一个方法和属性: 这种动态获取的信息以及动态调用对 ...

  5. Idea如何方便的查看Java字节码文件,你是怎么做的

    ·校园的傍晚· 前言 作为一名Java开发人员,我想Java字节码文件是无论如何都会接触到的,也是要读懂的.面试或者是自己开始研究Java的一些底层原理,大都会遇上要字节码文件的时候. 接下里咱们一起 ...

  6. 一个java源文件中可以声明多少个class与编译后会生成多少个字节码文件

    在一个java源文件中可以声明多个class. 但是,只能最多有一个类声明为public的. 而且要求声明为public的类的类名必须与源文件名相同. 编译的过程 编译以后,会生成一个或多个字节码文件 ...

  7. 什么是java源码文件,什么是字节码文件,初程序的编译和运行

    java源文件就是源代码文件,是指我们编写好的代码文件,即 当我们开始运行的时候系统会执行javac命令先把java源文件编译成字节码文件即: 这里科普一下java为什么会有字节码文件. 字节码产生的 ...

  8. 这一次,彻底弄懂 Java 字节码文件!

    作者 | 东升的思考 责编 | Elle 不啰嗦,直接从最最简单的一段Java源代码开启Java整体字节码分析之旅. Java 源码文件 package com.dskj.jvm.bytecode; ...

  9. Java反编译字节码文件

    如何查看 Java 的字节码文件? 在 Java 中,字节码文件.class实际上是二进制文件,并不能直接查看.要想查看,我们只能通过反编译对其进行解析,然后查看解析后的源码. 如何反编译字节码文件 ...

最新文章

  1. python3.6.4安装教程-python3.6.4如何安装到树莓派3代
  2. SpringBoot使用LibreOffice转换PDF
  3. 基于GraphHopper搭建离线路径规划服务并可视化
  4. 多线程还是多进程的选择及区别
  5. Vagrant安装centos7时一直报错无法保存文件
  6. 华为nove计算机在哪里,华为Nova手机备忘录怎么同步到电脑
  7. LUT Utility for FCPX(Luts调色文件加载工具)支持M1
  8. LeetCode 2099. 找到和最大的长度为 K 的子序列
  9. sublime html乱码,HTML5 sublime 编码出现乱码怎么处理
  10. (2)I/O流对象-----FilterInputStream与FilterOutputStream
  11. 微信公众平台开发(1)--账号注册流程图文详解
  12. python图像降采样,【图像处理】——改变图像的大小(降采样重采样)
  13. 快手加抖音自动刷视频脚本,引用的是python+易语言编程
  14. 微信网页版扫码登录是如何实现的?
  15. Sick编码器CanOpen通信
  16. 快手发布二季度及中期业绩:Q2收入191亿元 同比劲增48.8%
  17. qlv转MP4(笨方法,大用途)
  18. 全网最简单的RFM模型制作方法,3岁小孩都能学会!
  19. SAP小技巧之 标签打印
  20. Anaconda创建新环境及在pycharm中的设置

热门文章

  1. matlab 颜色势能,Matlab在模拟晶面势能分布中的应用
  2. excel填充序列_excel如何快速填充数据
  3. c mysql批量添加数据类型_mybatis学习之路----mysql批量新增数据
  4. 2.利用计算机进行信息加工的一般过程是:,[信息技术教案]《计算机信息加工的一般过程》教案...
  5. 三星r750美版刷Android,这是才是全面派 三星space monitor详解
  6. 字体 素材_最新2019毛笔书法字体素材合集!自创字体解决版权,甲方看都说好...
  7. maven多模块合并打包_Spring Boot 多模块项目实践(附打包方法)
  8. matlab电力系统潮流计算,大神们,求个电力系统潮流计算的matlab程序。
  9. Nginx安装及其编译命令、详细教程
  10. 期末考试前的预习,科目:化工设备与反应器(1)