javaagent 使用注意

news/2024/4/23 19:11:53/

前言

最近做项目,需要实现一个agent,实现运行过程替换字节码,当笔者实现这些功能时发现还是很多注意事项的。而且字节码的替换过程如果类的属性与方法升级了,那么加载就会报错。这种做法的好处是代码无侵入,缺点也很明显,严重依赖特定的jvm版本和中间件等。

javaagent简介

javaagent实际上是JVMTI使用的技术,核心依靠Instrumentation实现。查看这个包,官方文档:java.lang.instrument (Java Platform SE 8 )

其中一句很精髓:Provides services that allow Java programming language agents to instrument programs running on the JVM. The mechanism for instrumentation is modification of the byte-codes of methods. 提供服务,允许Java编程语言代理对JVM上运行的程序进行检测。检测的机制是修改方法的字节码。javaagent有2种实现,一种是jvm参数,一种是动态attach。

实现方式是addTransformer,只要是在addTransformer之前未被加载的类在加载的过程就会被我们自定义的字节码替换,如果已经加载的类需要替换,可以手动retransformClasses,当然也可以redefineClasses,不过就还原来讲,推荐retransformClasses。

准备demo及问题过程

准备字节码替换和demo,先替换一个jdk的类,比如要对File的list进行字节码替换。比如asm javassist等,javassist比较简单,而asm比较常用,比如cglib:https://asm.ow2.io/asm4-guide.pdf

 先用javassist试试

  • ClassPool:CtClass池,可使用classPool.get(类全名)获取CtClass
  • CtClass:   编译时类信息,class文件封装
  • CtMethod:类中的方法
  • CtField:    类中的属性、变量

写个Controller,触发条件

    @RequestMapping("/file")public String[] fileList() {File file = new File("/Users/huahua/go");return file.list();}

agent

    public class Agent {private static synchronized void initAgent(String args, Instrumentation inst) {System.out.println("agent exec ......");inst.addTransformer(new ClassFileTransformer() {@Overridepublic byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {//字节码修改,替换String refName = className.replace("/", ".");if (MethodFilter.filterClass(refName)) {try {return MethodFilter.getHook(refName).hookMethod(loader, className, classfileBuffer);} catch (NotFoundException | CannotCompileException | IOException e) {throw new RuntimeException(e);}}return classfileBuffer;}}, true);
//                Class<?> clazz = Class.forName("com.feng.agent.demo.ReTransformDemo");
//                inst.retransformClasses(clazz);System.out.println("agent exec end......");}public static void premain(String args, Instrumentation inst) {initAgent(args, inst);}public static void agentmain(String args, Instrumentation inst) {initAgent(args, inst);}}

hook逻辑

public interface MethodHook {byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException;}public class FileHook  implements MethodHook{public byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException {// TODO: 获取ClassPoolClassPool classPool = ClassPool.getDefault();
//        CtClass ctClass = classPool.get(className);CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));// TODO: 获取sayHelloFinal方法CtMethod ctMethod = ctClass.getMethod("list", "()[Ljava/lang/String;");// TODO: 方法前后进行增强ctMethod.insertBefore("{ System.out.println(\"start\");}");ctMethod.insertAfter("{ System.out.println(\"end\"); }");// TODO: CtClass对应的字节码加载到JVM里
//        Class c = ctClass.toClass();return ctClass.toBytecode();}}public class MethodFilter {private static Map<String, MethodHook> classMap = new HashMap<>();static {classMap.put("java.io.File", new FileHook());}public static boolean filterClass(String classname){return classMap.containsKey(classname);}public static MethodHook getHook(String classname) {return classMap.get(classname);}
}

问题1 :前置检查不生效

此时 触发第一个注意,已经加载的类必须主动retransformClasses才能生效,否则addTransformer是不会替换类的,addTransformer是前置检查,只有在类载入钱才能执行字节码替换

可以看到实际上类替换未生效,因为File类已经加载了,debug看原因

Arrays.stream(inst.getAllLoadedClasses()).filter((c)->c!=null&&c.getName().startsWith("java.io.File")).collect(Collectors.toList()) 

如下图,本次替换的File实际上已经加载了,未生效 ,常用的类还有输入输出流等

 

 解决办法也简单,在addTransformer之后加入retransformClasses即可生效

Class<?>[] classes = inst.getAllLoadedClasses();Arrays.stream(classes).filter((c) -> c!=null&& MethodFilter.filterClass(c.getName())).forEach((c)->{try {inst.retransformClasses(c);} catch (UnmodifiableClassException e) {throw new RuntimeException(e);}
});            

 测试加入代码后果然生效

 问题2:jdk的类替换问题

笔者这里使用的jdk自带的system.out,如果我自己写一个类呢,实际情况很常见。

public class FileCheck {public void checkFilePath(File file){if (file.getAbsolutePath().startsWith("/Users")) {System.out.println("user dir");}System.out.println("File start " + file.getPath());}
}

 ctMethod.insertBefore("{ FileCheck.checkFilePath(this);}");

会触发

javassist.CannotCompileException: [source error] no such class: FileCheck

因为修改的是JDK的类,但是JDK的类是bootstrap加载的,那么我们自己写的类呢

bootstrap的classloader是没办法加载AppClassloader的类的

所以需要 appendToBootstrapClassLoaderSearch,把我们写的类放进jdk的搜索范围,为此修改插桩技术,因为需要静态方法才好插桩,当然也可以用非静态方法,用反射插桩。

public class FileCheck {public static void checkFilePath(File file){if (file.getAbsolutePath().startsWith("/Users")) {System.out.println("user dir");}System.out.println("File start " + file.getPath());}
}public byte[] hookMethod(ClassLoader loader, String className, byte[] classfileBuffer) throws NotFoundException, CannotCompileException, IOException {// TODO: 获取ClassPoolClassPool classPool = ClassPool.getDefault();
//        CtClass ctClass = classPool.get(className);CtClass ctClass = classPool.makeClass(new ByteArrayInputStream(classfileBuffer));// TODO: 获取sayHelloFinal方法CtMethod ctMethod = ctClass.getMethod("list", "()[Ljava/lang/String;");// TODO: 方法前后进行增强ctMethod.insertBefore("{com.feng.agent.FileCheck.checkFilePath($0);}");ctMethod.insertAfter("{System.out.println(\"end\");}");// TODO: CtClass对应的字节码加载到JVM里
//        Class c = ctClass.toClass();return ctClass.toBytecode();}

修改后执行正常

 问题3:classloader的问题

经过上面的处理,虽然jdk的类可以替换了,但是是通过把agent的jar加到appendToBootstrapClassLoaderSearch搜索解决的,但是BootstrapClassLoader类加载器并不会加载一些额外的类,就会造成多次使用多次加载的现象。示例 如下

public class CheckStatus {private static Map<String, Boolean> statusMap = new HashMap<>();public static void initStatus(){statusMap.put("FILE_STATUS", true);}public static Boolean getStatus(String statusKey){if (!statusMap.containsKey(statusKey)) return false;return statusMap.get(statusKey);}
}

然后通过agent初始化

 然后在字节码替换的地方加入

public class FileCheck {public static void checkFilePath(File file){if (file.getAbsolutePath().startsWith("/Users")) {System.out.println("user dir");System.out.println("CheckStatus: " + CheckStatus.getStatus("FILE_STATUS"));}System.out.println("File start " + file.getPath());}
}

执行后发现CheckStatus的值是false

原因也很简单,因为appendToBootstrapClassLoaderSearch前的载入classloader是APPclassloader,但是appendToBootstrapClassLoaderSearch后使用的bootstrapclassloader,所以只要颠倒顺序即可解决

 

实际上应该把jdk替换和非jdk的区分,因为代码复用的情况,但是有时候又不能严格区分,此时就会有矛盾的处理,因为双亲委派和依赖加载,所以很多时候是自定义classloader,把agent的核心jar用自定义classloader反射执行。但是涉及jdk相关的类需要使用jdk原有逻辑加载

public class AgentClassloader extends URLClassLoader {public AgentClassloader(URL[] urls) {super(urls, ClassLoader.getSystemClassLoader().getParent());}@Overrideprotected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {final Class<?> loadedClass = findLoadedClass(name);if (loadedClass != null) {return loadedClass;}// 优先从parent(SystemClassLoader)里加载系统类,避免抛出ClassNotFoundExceptionif (name != null && (name.startsWith("sun.") || name.startsWith("java."))) {return super.loadClass(name, resolve);}try {Class<?> aClass = findClass(name);if (resolve) {resolveClass(aClass);}return aClass;} catch (Exception e) {// ignore}return super.loadClass(name, resolve);}
}

总结

实际上agent本身的技术很简单,但是涉及类加载就复杂多了,类有classloader,线程有classloader,而且线程的classloader和类的可以不一样,当子classloader加载可以去parent里面去找,但是parent不能向下查找,此时就只能自己加载。

另外agent的原理是类加载前执行替换,那么一些jdk的类就会出现替换失败,且jdk的类是bootstrapclassloader加载的,所以经常容易处理不好,加载异常,需要把jdk替换的相关类加入bootstrap查找,而且appclassloader或者自定义加载的bootstrap还会重复加载。


http://www.ppmy.cn/news/46872.html

相关文章

在一个maven项目中使用maven命令进行junit单元测试

如何在一个maven项目中使用maven命令进行junit单元测试? 首先确定一个maven项目的结构: 包含源代码目录src/main/java.配置目录src/main/resources.测试代码目录src/test. 目录结构可视化如下: src/- main/- java/com/example/samplejunit- demo.java- resources/com/exam…

Ansys Speos | 实现车内氛围灯早期仿真验证

在本例中&#xff0c;将演示如何使用Speos进行RGB(红、绿、蓝)车内环境照明的早期研究&#xff0c;目的是在设计光导之前评估指定位置的照明效果。 使用到的产品Ansys Speos 2022 R2或更高版本&#xff0c;license为Enterprise版本能激活人眼视觉效果。 概览 在汽车行业&#…

C++11新特性有效总结

目录 语言可用性加强 (读现代C教程有感) nullptr constexpr if/switch 申明强化 &#xff08;C17开始&#xff09; 初始化参数列表 范围for迭代 两种类型推导方式 变长参数模板 SmartPointer Lambda 多线程 (并发与并行) 并发与并行的概念 C11中的并发并行 软件…

stm32cubemx IAP升级(三)

stm32cubemx IAP升级- UARTDMA实现不定长收发数据 板卡&#xff1a;Nucleo-L412 平台&#xff1a;macbook pro 工具&#xff1a;vscode stm32cubemx stm32cubeProgramer cmake toolchain Stm32CubeMx的配置 选择开启一路串口并配置成DMA&#xff0c;并使能中断&#xff0c;配…

第三十章 配置镜像 - 激活和更新镜像数据库

文章目录 第三十章 配置镜像 - 激活和更新镜像数据库激活和更新镜像数据库编辑或删除镜像成员清除报告异步镜像成员上的 FailoverDB 标志 第三十章 配置镜像 - 激活和更新镜像数据库 激活和更新镜像数据库 可以使用镜像监视器在备份故障转移成员和异步成员上激活和/或赶上镜像…

QPSK调制解调FPGA实现成果展示:

目录 QPSK调制解调使用参数&#xff1a; 调制&#xff1a; 解调&#xff1a; FPGA工程架构&#xff1a; 仿真参数&#xff1a; 仿真展示&#xff1a; 调制&#xff1a; 解调&#xff1a; MATLAB星座图展示&#xff1a; QPSK调制解调使用参数&#xff1a; 采样率为4M&…

ChatGPT研究报告:AIGC带来新一轮范式转移

本文约4000字&#xff0c;目标是快速建立AIGC知识体系&#xff0c;含有大量的计算专业名词&#xff0c;建议阅读同时扩展搜索。 一、行业现状 1、概念界定 区别于PGC与UGC不同的&#xff0c;AIGC是利用人工智能技术自动生成内容的新型生产方式。 2、数据模态 按照模态区分&a…

01-java基本概念

1、语言背景 Java语言是美国Sun公司&#xff08;Stanford University Network&#xff09;在1995年推出的计算机语言 Java之父&#xff1a;詹姆斯高斯林&#xff08;James Gosling&#xff09; 2009年&#xff0c;Sun公司被甲骨文公司收购&#xff0c;所以我们现在访问oracle官…

Unity 边缘光

文章目录 边缘光是什么实现原理代码 refer&#xff1a; 参考1 参考2 边缘光是什么 边缘光的用处很广泛&#xff0c;比如材质的渲染&#xff0c;云的渲染&#xff0c;角色选中特效等 边缘光也是描边的一种 下面这两张就是添加边缘光后的对比&#xff1a; 这样的效果在这种玉石…

【Linux内核解析-linux-5.14.10】进程管理

1. 进程管理 进程管理主要包括&#xff1a; 进程调度&#xff1a;Linux内核中的进程调度机制&#xff0c;包括进程调度策略、调度器、进程状态等。进程创建&#xff1a;Linux内核中的进程创建机制&#xff0c;包括fork()、exec()等函数的实现原理。进程通信&#xff1a;Linux…

代码随想录算法训练营第五十九天| 单调栈 503 下一个更大元素II 42 接雨水

代码随想录算法训练营第五十九天| 单调栈 503 下一个更大元素II 42 接雨水 LeetCode 503 下一个更大元素II 题目: 503.下一个更大元素II 思路&#xff1a;本题重点在于如何处理循环数组&#xff0c;首先想到将两个数组拼在一起&#xff0c;然后使用单调栈求下一个最大值 方…

打造安全无忧软件应用的十大最佳实践

安全无忧的软件开发最佳实践实在是很有必要&#xff0c;因为安全风险无处不在。在网络攻击盛行的时代&#xff0c;它们可能影响到每个人&#xff0c;包括个人、公司和政府。因此&#xff0c;确保软件开发的安全性至关重要。 本篇文章将解释了什么是安全的软件&#xff0c;如何…

[SSD核心技术:FTL 17] 固态硬盘掉电保护也是一门艺术 | 掉电保护原理与抉择 | 掉电保护测试

声明 主页:元存储的博客_CSDN博客 依公开知识及经验整理,如有误请留言。 个人辛苦整理,付费内容,禁止转载。 内容摘要 全文4800字, 阅读大约 24 分钟 前言 1 系统掉电的灾难

一起学 WebGL:三角形加上渐变色

大家好&#xff0c;我是前端西瓜哥。之前教大家绘制一个红色的三角形&#xff0c;这次我们来画个有渐变的三角形。 本文为系列文章&#xff0c;请先阅读如何绘制红色三角形的文章&#xff1a; 《一起学 WebGL&#xff1a;绘制三角形》 原来的写法&#xff0c;颜色是在片元着色器…

恢复误删文件

误删恢复 用losf恢复进程存在的文件 注意此处要后台进程存在 创建一个文件&#xff0c;用tail命令&#xff0c;模拟文件一直被监听 打开另外一个终端&#xff0c;删除这个文件 用lsof命令查看被删除的文件&#xff0c;可以发现文件虽然被删除&#xff0c;但是进程依然在 然后…

16 个优秀的 Vue 开源项目

为什么我们要关注Vue Vue是一个用于构建用户界面的JavaScript框架。值得关注的是&#xff0c;它在没有谷歌和Facebook的支持下获得了大量的人气。 Vue是结合react和angular的最好的方法&#xff0c;并且拥有一个有凝聚力的&#xff0c;活跃的&#xff0c;能够应对开发问题的大型…

2023/4/13总结

最小生成树 一、Prim算法 1.prim算法也被称为“加点法”&#xff0c;因为该算法是先从任意一顶点出发不断的选择目前距离最近且未被选择的点加入到已选的集合中&#xff0c;直到所有的点都被选到。&#xff08;和最短路径中的Dijkstra算法很像&#xff09; 2.prim算法的实现…

文件操作【下篇】

文章目录 &#x1f5c3;️5.文件的随机读写&#x1f4c1;5.1. fseek&#x1f4c1;5.2. ftell&#x1f4c1;5.3. rewind &#x1f5c3;️6.文本文件和二进制文件&#x1f5c3;️7.文件读取结束的判定&#x1f4c1;7.1. 被错误使用的 feof &#x1f5c3;️8.文件缓冲区 &#x1f…

折叠屏市场起风,华为、OPPO“你追我赶”

配图来自Canva可画 现如今&#xff0c;智能手机已经成为了人们生活中不可或缺的重要工具&#xff0c;无论是出行&#xff0c;还是社交&#xff0c;亦或是支付&#xff0c;只需要一部智能手机就可以通通搞定。因此&#xff0c;在消费者多样化需求的助推下&#xff0c;智能手机行…

研读Rust圣经解析——Rust learn-3(变量与可变性,数据类型)

研读Rust圣经解析——Rust learn-3&#xff08;变量与可变性&#xff0c;数据类型&#xff09; 变量|常量与可变性变量声明案例为什么不可变变量可变&#xff08;mut关键字&#xff09;变量可变&#xff08;覆盖&#xff09; 常量声明 数据类型标量类型整型整型字面值整型溢出问…