Java字节码插桩玩法(Javassist)
1. 搭建一个工程测试premain在main之前执行
需求很简单,只需要让premain方法在main之前输出就行
- premain类
package com.wql;import java.lang.instrument.Instrumentation;/*** @author wql* @date 2021/11/28 15:59*/
public class JavaAgentMain {public static void premain(String arg, Instrumentation instrumentation) {System.out.println("我是premain方法");}
}
- HelloWql类
package com.wql;/*** @author wql* @date 2021/11/28 15:09*/
public class HelloWql {public void helloWql(String name) {System.out.println("name");}
}
- TestJavaAgentMain
package com.wql.test;/*** @author wql* @date 2021/11/28 16:01*/
public class TestJavaAgentMain {public static void main(String[] args) {System.out.println("我是main方法");}
}
- pom 文件
因为我是借助maven打包普通工程的,所以maven里面就没有啥依赖目前
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>testAgent</groupId><artifactId>testAgent</artifactId><version>1.0.0-SNAPSHOT</version><build><plugins><!--maven打包插件--><!--maven-jar-plugin用于生成META-INF/MANIFEST.MF文件的部分内容--><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><version>2.2</version><configuration><classesDirectory>target/classes/</classesDirectory><archive><manifestEntries><Project-name>${project.name}</Project-name><Project-version>${project.version}</Project-version><Premain-Class>com.wql.JavaAgentMain</Premain-Class><Can-Redefine-Classes>false</Can-Redefine-Classes><Can-Retransform-Classes>false</Can-Retransform-Classes></manifestEntries></archive><skip>true</skip></configuration></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>2.3.1</version><configuration><source>1.8</source><target>1.8</target><encoding>utf-8</encoding></configuration></plugin></plugins></build></project>
- 打包
- 在测试类里面配置生成的javaahent的jar包
- 启动测试类测试:
2. 需求2
在helloWql方法执行之前打印当前时间
2.1 Javassist介绍
Javassit是一个开源的分析,编辑,和创建Java字节码的类库,其主要优点在于简单,而且快速,直接使用java编码的形式,而不需要了解java虚拟机指令,就能动态改变类结构,或者动态生成
注: 也可以使用ASM实现,但是需要学会操作字节码指令,学习成本高
运行时.jar | javaagent.jar | |
---|---|---|
入口方法 | main(String[]) | premain(String,instrumcent) |
参数 | main-class | premain-class |
运行 | java-jar xxx.jar | 只能附着于其他程序运行 -javaagent:xxx.jar |
所以我们可以采用Javassis技术来完成插桩
<dependency><groupId>org.javassist</groupId><artifactId>javassist</artifactId><version>3.18.1-GA</version></dependency>
官方文档: http://www.javassist.org/tutorial/tutorial2.html
2.2 使用javassit完成需求2
public class JavaAgentMain {public static void premain(String arg, Instrumentation instrumentation) {System.out.println("premain执行了");//基于javassist工具,在运行时修改class字节码,就是所谓的插桩final ClassPool classPool = new ClassPool();classPool.appendSystemPath();instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {// 注意,这里是以/结尾if (!"com/wql/HelloWql".equals(className)) {return new byte[0];}//千万不要使用 HelloWql.class.getName() ,因为使用了就会加载,不会走插桩CtClass ctClass = null;try {// 获取类ctClass = classPool.get("com.wql.HelloWql");//获取类里面的方法CtMethod helloWql = ctClass.getDeclaredMethod("helloWql");//在方法前加入指令码helloWql.insertBefore("System.out.println(\"System.currentTimeMillis() = \" + System" +".currentTimeMillis());");return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}return new byte[0];});}
}
重新打包运行,结果如下:
3. 需求3
在方法前面添加时间并且计算时间差
注意,插桩插入的是一个个代码块
举例:
这样写会报以下错误:
因为上面的代码就等于下面这种:
每一个{}都代表一个模块,变量是不可用跨模块的。所以会报变量end找不到的错误
所以我们会采用如下思想:
复制一个代理方法出来,就可以搞定,映射到字节码插桩里面,javassit也可以这样玩
public class JavaAgentMain {public static void premain(String arg, Instrumentation instrumentation) {System.out.println("premain执行了");//基于javassist工具,在运行时修改class字节码,就是所谓的插桩final ClassPool classPool = new ClassPool();classPool.appendSystemPath();instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {// 注意,这里是以/结尾if (!"com/wql/HelloWql".equals(className)) {return new byte[0];}//千万不要使用 HelloWql.class.getName() ,因为使用了就会加载,不会走插桩CtClass ctClass = null;try {// 获取类ctClass = classPool.get("com.wql.HelloWql");//获取类里面的方法CtMethod helloWql = ctClass.getDeclaredMethod("helloWql");//在方法前加入指令码//1. 复制方法CtMethod copy = CtNewMethod.copy(helloWql, ctClass, null);// 给新的方法换一个名字copy.setName("helloWql$Agent");//将这个方法添加到类里面ctClass.addMethod(copy);// 2. 改变原有方法helloWql.setBody(" {long satrt = System.currentTimeMillis();\n" +"helloWql$Agent($1);" +" long end = System.currentTimeMillis();\n" +" System.out.println(\"end - satrt = \" + (end - satrt));}");return ctClass.toBytecode();} catch (Exception e) {e.printStackTrace();}return new byte[0];});}
}
注意几个细节:
- 每一行代码执行完必须加上;结束符
- 代码块需要使用{}包起来
- 方法与方法之间传参,参数使用$0 $1 …表示,$0代表this
4. 需求4
假设现在项目里面有100个方法,我想检测每一个方法的执行时间,该怎么操作
package com.wql;import javassist.*;import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.util.Objects;/*** @author wql* @date 2021/11/28 15:59*/
public class JavaAgentMain {public static void premain(String arg, Instrumentation instrumentation) {final String config = arg;final ClassPool classPool = new ClassPool();classPool.appendSystemPath();instrumentation.addTransformer((loader, className, classBeingRedefined, protectionDomain, classfileBuffer) -> {if (className == null || !className.replaceAll("/", ".").startsWith(config)) {return new byte[0];}className = className.replaceAll("/", ".");try {CtClass ctClass = classPool.get(className);for (CtMethod declaredMethod : ctClass.getDeclaredMethods()) {newMethod(declaredMethod);}return ctClass.toBytecode();} catch (NotFoundException e) {e.printStackTrace();} catch (CannotCompileException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}return new byte[0];});}final static String source = "{ long begin = System.currentTimeMillis();\n" +" String method = Thread.currentThread() .getStackTrace()[1].getMethodName();"+" Object result;\n" +" try {\n" +" result = ($w)%s$Agent($$);\n" +" } finally {\n" +" long end = System.currentTimeMillis();\n" +" System.out.println(\"方法名:\"+method+\"=> 耗时:\"+(end-begin)+\"ms\");\n" +" }\n" +" return ($r)result;\n}";final static String voidSource = "{ long begin = System.currentTimeMillis();\n" +" String method = Thread.currentThread() .getStackTrace()[1].getMethodName();"+" try {\n" +" %s$Agent($$);\n" +" } finally {\n" +" long end = System.currentTimeMillis();\n" +" System.out.println(\"方法名:\"+method+\"=> 耗时:\"+(end-begin)+\"ms\");\n" +" }\n" +" }";private static CtMethod newMethod(CtMethod oldMethod) throws CannotCompileException, NotFoundException {CtMethod copy = CtNewMethod.copy(oldMethod, oldMethod.getDeclaringClass(), null);copy.setName(oldMethod.getName() + "$Agent");//添加新方法oldMethod.getDeclaringClass().addMethod(copy);if (oldMethod.getReturnType().equals(CtClass.voidType)) {oldMethod.setBody(String.format(voidSource, oldMethod.getName()));} else {oldMethod.setBody(String.format(source, oldMethod.getName()));}return copy;}
}
修改以下插桩写法就行,上面代码有几个地方解释一下
- $$ 表示多个参数,arg1,arg2,arg3
- $1 表示 arg1
- $2 表示 arg2
- $args 表示 object[]
- $w 表示类型转换 eg: (String)object
- $r 表示返回值
最后记得配置一下参数就行
Java字节码插桩玩法(Javassist)相关推荐
- 字节码插桩(javassist)之插入代码块|IOC框架(Hilt)之对象注入~研究
Hilt对象注入 | javassist插桩 研究 Hilt对象注入 javassist字节码插桩 创建buildSrc的module 重写Transform 熟悉TransformInvocatio ...
- Java ASM框架与字节码插桩的常见用法(生成类,修改类,方法插桩,方法注入)
前言 ASM 是一款读写Java字节码的工具,可以达到跳过源码编写,编译,直接以字节码的形式创建类,修改已经存在类(或者jar中的class)的属性,方法等. 通常用来开发一些Java开发的辅助框架, ...
- 字节码插桩之Java Agent
字节码插桩之Java Agent 本篇文章将详细讲解有关Java Agent的知识,揭开它神秘的面纱,帮助开发人员了解它的黑魔法,帮助我们完成更多业务需求 What is Java Agent Jav ...
- 调研字节码插桩技术,用于系统监控设计和实现
作者:小傅哥 博客:https://bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获!???? ❞ 目录 一.来自深夜的电话! 二.准备工作 三.使用 AOP 做个切面监控 1. ...
- Android AOP之字节码插桩
背景 本篇文章基于<网易乐得无埋点数据收集SDK>总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实 ...
- aop 获取方法入参出参_ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称及入参和出参结果并记录方法耗时...
作者:小傅哥 博客:bugstack.cn ❝ 沉淀.分享.成长,让自己和他人都能有所收获! ❞ 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了 ...
- 看完这一篇,你也可以自如地掌握字节码插桩
/ 今日科技快讯 / 近日,一些国家的黑客频繁对俄罗斯发动网络攻击,以阻止它们正常运行.未来几天,俄罗斯可能与全球互联网断开.针对网络威胁,俄罗斯政府准备启动自己的"大局域网&quo ...
- Android字节码插桩
什么是字节码插桩 字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加. 简单来讲,我们要实现无埋点对客户端的全量统计.这里的统计概括的范围比较广泛, ...
- ASM字节码编程 | JavaAgent+ASM字节码插桩采集方法名称以及入参和出参结果并记录方法耗时
作者:小傅哥 博客:bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 在我们实际的业务开发到上线的过程中,中间都会经过测试.那么怎么来保证测试质量呢?比如:提交了多少代码 ...
- 字节码插桩(四): AST
我们通过 AndroidStudio 生成Bean对象一般是通过注解来实现自动生成getter/setter方法.equals()和hashCode()方法,其中类(或接口)要符合驼式命名法,首字母大 ...
最新文章
- 算法笔记_218:花朵数(Java)
- mysql 5.7 存储引擎_mysql5.7——innodb存储引擎总结
- 查看电脑python虚拟环境-Linux系统创建python虚拟环境
- Vue.js——60分钟快速入门
- java:socket通信
- 推荐系统笔记:无任何限制的矩阵分解
- RabbitMQ——无法连接错误[AmqpConnectException: java.net.ConnectException: Connection refused: connect]解决方案
- 【博客话题】技术生涯中的出与入
- linux 查看端口 程序,linux开发:Linux下查看端口占用
- LeetCode 127. 单词接龙(图的BFS/双向BFS)
- 常用函数式接口-Consumer
- Werkzeug Turorial
- c语言数组中循坏移动问题,如何将一个数组的元素循环左移?
- 如何将IP地址批量改变为城域网的IP地址
- linux 上如何测速
- linux添加静态ipv6路由,请问如何在CentOS7上配置已经静态路由好的IPv6地址块?
- 等保-机房项目验收方法
- 提示502的解决办法
- viewpager 与 pageradapter
- python爬取阿里巴巴网站实现
热门文章
- 开源加速器Gemmini代码解析(一):脉动阵列
- 【芝麻背调百科】已婚员工入职新公司后申请休婚假,公司能否拒绝?
- 【进击全栈 2,毕向东Java教程百度云
- telnet 命令参数及其应用方式
- 修改 xweibo 的memcache代码,让xweibo支持wincache,加快xweibo速度
- 泛微oa系统服务器怎么填写,泛微协同办公平台Ecology8安装部署手册(105页)-原创力文档...
- NB-IoT 无线烟感在文物古建筑的防火安全应用
- 串口转以太网使用方法
- 计算机网络(二)—— 物理层(1、2、3):物理层的基本概念、物理层的下面的传输媒体、传输方式
- hbase 2.0.5的下载及安装