Java agent 探针技术(1)-JVM 启动时 premain 进行类加载期增强
文章目录
- 1. 简介
- 2. 使用 Java agent 的步骤
- 3. 使用示例
- 3.1 创建实现 ClassFileTransformer 接口的类
- 3.2 创建使用 ClassFileTransformer 的 premain 类
- 3.3 打包代理 jar 包
- 3.4 测试
1. 简介
在之前的文章 静态代理 一节中我们已经提到过 Java 探针技术,简单来说,在 JDK 1.5
中 Java 引入了 java.lang.Instrument
包,该包提供了一些工具使得在类加载时期修改 Class 类成为了可能。这实际上就是提供了一种虚拟机级别的 AOP,其基本的原理可依据下图阐明:
在 类加载过程 一文我们提到了类加载的过程,其第一步就是加载。其实从 Java 类完整的生命周期来看,从 Java 源文件到虚拟机运行时的 Class 类,这中间还存在不少的处理过程,大致可分为如下两步。其中 Java agent 拦在 JVM 和运行时 Class 类之间,就相当于一个切面,为我们增强类功能提供了一个切入点
- 首先 Java 文件要经过
编译器
编译成为 Class 字节码文件- Class 字节码文件经过 IO 读到
JVM
中,JVM 经过解析验证等环节最终创建出运行时的 Class 类
2. 使用 Java agent 的步骤
Java agent 的使用需要如下几个步骤:
创建一个指定的类作为
Premain-Class
,类中包含premain()
方法,该方法有如下两个声明。JVM 会优先加载方法1,加载成功忽略 2,如果1 没有,则加载 2 方法
public static void premain(String agentArgs, Instrumentation inst)
:参数 agentArgs 是通过命令行传给 Java agent 的参数, inst 是 Java 的字节码转换工具public static void premain(String agentArgs)
创建
MANIFEST.MF
配置文件,将Premain-Class
指定为包含premain()
方法的类。该配置文件通常也会将Can-Redefine-Classes
和Can-Retransform-Classes
配置为 true将包含
premain()
方法的类和MANIFEST.MF
文件打包成代理 jar 包使用
java -javaagent:<jarpath>[=options] -jar xxx.jar
命令启动一个 Java 程序,并为其指定代理 jar 包
在执行第4个步骤后,目标 Java 程序启动执行 main()
方法之前,会先运行 -javaagent
参数指定的代理 jar 包内 Premain-Class
类的 premain()
方法
大部分类加载都会在
main()
方法执行之后进行,这样premain()
方法就能拦截大部分类的加载活动。没拦截到的主要是系统类,因为很多系统类必须提前加载完成,用户类的加载肯定是在premain()
方法执行之后进行的
3. 使用示例
3.1 创建实现 ClassFileTransformer 接口的类
创建一个 CustomTransformer
类,该类实现了 ClassFileTransformer
接口并重写了 ClassFileTransformer#transform()
方法,主要实现的功能是为sample.ReactorMain#deal()
方法添加了执行耗时打印,需要注意的点如下:
- 该实现中默认会修改静态变量
DEFAULT_METHOD
保存的指定类的指定方法的字节码,修改字节码依赖的工具为 javaassist- 本实现修改方法字节码的方式是基于原来的方法
deal
复制出一个新方法,然后修改原方法名为 deal$old,最后再重新设置复制出的方法的方法体,替换为原来的方法deal
。这个过程中产生了一个新的方法deal$old
,需注意premain
这种增强方式允许修改字节码添加新方法,agentmain
则不允许- 如果存在多个 agent 修改同一个类同一个方法的字节码,需注意修改过程中产生的方法不能出现重复命名,否则会报错
Duplicate method name "deal$old" with signature "()V" in class file sample/ReactorMain
import javassist.*;import java.lang.instrument.ClassFileTransformer;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;/*** 检测方法的执行时间*/
public class CustomTransformer implements ClassFileTransformer {// 被处理的方法列表private final static Map<String, List<String>> METHOD_MAP = new ConcurrentHashMap<>();private static final String DEFAULT_METHOD = "sample.ReactorMain.deal";private static final String CLASS_REGEX = "^(\\w+\\.)+[\\w]+$";private static final Pattern CLASS_PATTERN = Pattern.compile(CLASS_REGEX);private CustomTransformer() {add(DEFAULT_METHOD);}public CustomTransformer(String methodString) {this();if (!CLASS_PATTERN.matcher(methodString).matches()) {System.out.println("string:" + methodString + " not a method string");return;}add(methodString);}public void add(String methodString) {String className = methodString.substring(0, methodString.lastIndexOf("."));String methodName = methodString.substring(methodString.lastIndexOf(".") + 1);List<String> list = METHOD_MAP.computeIfAbsent(className, k -> new ArrayList<>());list.add(methodName);}@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,ProtectionDomain protectionDomain, byte[] classfileBuffer) {className = className.replace("/", ".");byte[] byteCode = null;// 判断加载的class的包路径是不是需要监控的类if (METHOD_MAP.containsKey(className)) {CtClass ctClass;try {ClassPool classPool = ClassPool.getDefault();// 将要修改的类的classpath加入到ClassPool中,否则可能找不到该类classPool.appendClassPath(new LoaderClassPath(loader));ctClass = ClassPool.getDefault().get(className);for (String methodName : METHOD_MAP.get(className)) {// 得到方法实例CtMethod ctMethod = ctClass.getDeclaredMethod(methodName);// 创建新的方法,复制原来的方法,名字为原来的名字CtMethod newMethod = CtNewMethod.copy(ctMethod, methodName, ctClass, null);// 定义一个方法名用于描述修改字节码之前的原方法String oldMethodName = methodName + "$old";// 将原方法名称修改掉,避免和新添加的方法同名冲突ctMethod.setName(oldMethodName);// 构建新的方法体StringBuilder bodyStr = new StringBuilder();bodyStr.append("{");bodyStr.append("long startTime = System.currentTimeMillis();\n");// 调用原方法代码,类似于method();($$)表示所有的参数bodyStr.append(oldMethodName).append("($$);\n");bodyStr.append("long endTime = System.currentTimeMillis();\n");String outputStr = "System.out.println(\"this method " + methodName+ " cost:\" +(endTime - startTime) +\"ms.\");\n";bodyStr.append(outputStr);bodyStr.append("}");// 设置新的目标方法的方法体newMethod.setBody(bodyStr.toString());// 增加新方法, 原来的方法已经被修改名称为 oldMethodName,调用时会调用到新的目标方法ctClass.addMethod(newMethod);}byteCode = ctClass.toBytecode();// ClassPool中删除该类ctClass.detach();} catch (Exception e) {System.out.println(e.getMessage());e.printStackTrace();}}return byteCode;}
}
3.2 创建使用 ClassFileTransformer 的 premain 类
创建 InstrumentMain
类,该类需要重点关注的是两个 premain()
方法。可以看到主要逻辑是在两个入参的 premain()
方法中调用 Instrumentation#addTransformer()
方法,将自定义的 CustomTransformer
字节码转码器添加进去。这样在Java 程序 main()
方法执行前,每装载一个类ClassFileTransformer#transform()
方法就执行一次,从而检查加载的类是否需要转换
public class InstrumentMain {/*** 该方法在main方法之前运行,与main方法运行在同一个JVM中 并被同一个System ClassLoader装载* 被统一的安全策略(security policy)和上下文(context)管理*/public static void premain(String agentOps, Instrumentation inst) {System.out.println("====premain 方法执行开始");System.out.println(agentOps);inst.addTransformer(new CustomTransformer(agentOps));System.out.println("====premain 方法执行结束");}public static void premain(String agentOps) {System.out.println("====premain 方法执行开始");System.out.println(agentOps);System.out.println("====premain 方法执行结束");}public static void main(String[] args) {}
}
3.3 打包代理 jar 包
开发的最后一步是将包含 premain()
方法的类所在模块和 MANIFEST.MF
文件打包成代理 jar 包。IDEA 下打包 jar 包可参考博客 IDEA 打包 jar 包记录,最后创建的 MANIFEST.MF
文件内容如下,注意需要保留最后一行的空行
Manifest-Version: 1.0
Premain-Class: sample.InstrumentMain
Can-Redefine-Classes: true
3.4 测试
将如下目标类 ReactorMain
也打包成一个 jar 包,其 MANIFEST.MF
文件如下, 命名为 srcjar.jar
Manifest-Version: 1.0
Main-Class: sample.ReactorMain
public class ReactorMain {public static void main(String[] args) throws InterruptedException {deal();}public static void deal() throws InterruptedException {List<Integer> list = new ArrayList<>();list.add(1);list.add(2);int poolSize = 1;Long start = System.currentTimeMillis();CountDownLatch downLatch = new CountDownLatch(poolSize);Disposable disposable = Flux.range(1, 1000).onBackpressureBuffer().publishOn(Schedulers.elastic()).subscribe(null, null, downLatch::countDown);downLatch.await();disposable.dispose();Long end = System.currentTimeMillis();System.out.println("Duration:" + (end - start));}
}
代理 jar 包命名为 src.jar,则根据笔者 jar 包所在路径,最后的启动命令如下,可以看到修改的方法正常打印了执行耗时
java -javaagent:/Users/xxxxxx/workspace/demo/out/artifacts/src/src.jar=hello1 -jar /Users/xxxxxx/workspace/demo/out/artifacts/srcjar/srcjar.jar
Java agent 探针技术(1)-JVM 启动时 premain 进行类加载期增强相关推荐
- Java Agent 探针技术
Java 中的 Agent 技术可以让我们无侵入性的去进行代理,最常用于程序调试.热部署.性能诊断分析等场景,现如今比较火热的分布式链路追踪项目Skywalking,就是通过探针技术去捕获日志,将数据 ...
- Java Agent探针技术
1.基本概念 Java Agent 是 jdk1.5 引入的特征,此特征为用户提供了在 jvm 将字节码文件读入内存后,jvm 使用对应的字节流在 java 堆中生成 Class 对象之前,用户可以对 ...
- 深入Java自动化探针技术的原理和实践
转至作者 蒋志伟:深入Java自动化探针技术的原理和实践 前言 建议阅读时间 30~40分钟 读者需要对Java JVM 一定了解,文章会系统的介绍Java 探针核心原理和技术实现,总结目前一些主流的 ...
- Java Agent的隔离实现以及卸载时一些坑
转载自 Java Agent的隔离实现以及卸载时一些坑 在<一文带你了解Java Agent>中,让大家了解了Java Agent的来龙去脉,当通过attach方式去动态加载一个Jav ...
- 【Java综合专栏】「引领序幕」全链路追踪原理之Java Agent探针的技术介绍(上篇)
前提概要 Java调式.热部署.JVM背后的支持者Java Agent: 各个 Java IDE 的调试功能,例如 eclipse.IntelliJ : 热部署功能,例如 JRebel.XRebel. ...
- java 线程栈大小配置,JVM运行时数据区详解-Stack栈(优化配置、代码样例)
最近有段时间没有更新Netty的教程了,却发了一些其他的东西.可能有的朋友会问,难道这就完事了?不会的.两方面原因.第一.笔者也是需要工作的人,自然要完成好工作中的任务,这里面也有很多东西需要学习和研 ...
- 【JAVA基础☞探针技术】Java探针-Java Agent技术
个人博客导航页(点击右侧链接即可打开个人博客):大牛带你入门技术栈 1.原理:基于javaAgent和Java字节码注入技术的java探针工具技术原理 2.原理分析 动态代理功能实现说明,我们利用ja ...
- 在没有任何数据时进行无效的读取尝试。_技术转载——JVM运行时内存是怎么分布的?...
写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下! GitHub地址:https://github.com/ ...
- java xms xmx 默认值,JVM启动参数-Xmx的默认值是多少?
你也可以在程序里试试打印 Runtime.getRuntime().maxMemory() 的值 看看是多少 官网说明: https://docs.oracle.com/javase/8/docs/t ...
最新文章
- 36招搞定电脑一切难题
- 关于计算机组成的ppt,计算机组成课件.ppt
- Rdd没法调用toDF原因
- flowjo软件使用方法_管家婆软件使用方法出库教程,管家婆软件做账流程视频_双全科技...
- 引用参数与引用返回值
- mysql.sock 初始化_mysql8.0 部署、初始化和创建实例
- c语言 参数经过运算后还原为输入值,1:编程实现由键盘输入两个整数,将其赋给变量x和y并输出,交换x和y的值后再输出 用函数输出!...
- 常用shell命令介绍
- TCP协议中的三次握手和四次挥手(图解)【转】
- mysql innodb 浅读
- cad填充密度怎么调整_CAD填充图案时无法正常填充看似封闭的区域怎么解决?
- mysql字典_mysql常用字典表(完整版)
- CAD机械制图入门知识
- babyion 加载obj模型_如何使用BabylonJS加载OBJ或STL模型
- php会员中心页面,PhpCMS会员中心操作说明
- 全球及中国毫米波人体安检仪行业竞争格局分析与投资战略规划研究报告2022-2028年
- 商品进销差价_商品进销差价如何计算及账务处理怎么做?
- 一位知名 Python 技术博主因病离世
- 10:统计输入字符串中的单词个数及单词的平均长度
- MySQL 命令窗口
热门文章
- Linux终端的使用
- Java之jsp标签
- 【Spring注解驱动开发】java的类库在哪个文件夹
- win7映射网络驱动器消失了_win7系统无法映射网络驱动器的解决方法
- pytorch 实现k-means聚类算法
- a7芯片能跑linux吗,【分析】A7芯片:真的没区别?
- 含有计算机专业词的告别文案,适合告别的文案
- 四川省专业技术职称计算机,四川评职称将不再统一要求外语计算机
- u盘启动linux只有光标闪烁,deepin官方论坛-深度科技官网旗下网站
- java System.out.print();在控制台上修改输出颜色