JVM插码之三:javaagent介绍及javassist介绍
本文介绍一下,当下比较基础但是使用场景却很多的一种技术,稍微偏底层点,就是字节码插庄技术了...,如果之前大家熟悉了asm,cglib以及javassit等技术,那么下面说的就很简单了...,因为下面要说的功能就是基于javassit实现的,接下来先从javaagent的原理说起,最后会结合一个完整的实例演示实际中如何使用。
1、什么是javassist?
Javassist是一个开源的分析、编辑和创建Java字节码的类库。其主要的优点,在于简单,而且快速。直接使用java编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成
2、Javassist 作用?
a. 运行时监控插桩埋点
b. AOP动态代理实现(性能上比Cglib生成的要慢)
c. 获取访问类结构信息:如获取参数名称信息
3、Javassist使用流程

4、 如何对WEB项目对象进行字节码插桩
1.统一获取HttpRequest请求参数插桩示例
2.获取HttpRequest参数遇到ClassNotFound的问题
3.Tomcat ClassLoader 介绍,及javaagent jar包加载机制
4.通过class 加载沉机制实现在javaagent 引用游jar 包
javaagent的主要功能有哪些?
- 可以在加载java文件之前做拦截把字节码做修改
- 获取所有已经被加载过的类
- 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
- 获取某个对象的大小
- 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
- 将某个jar加入到classpath里供AppClassloard去加载
- 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配
JVMTI
JVM Tool Interface,是jvm暴露出来的一些供用户扩展的接口集合,JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者去扩展自己的逻辑。
比如说我们最常见的想在某个类的字节码文件读取之后类定义之前能修改相关的字节码,从而使创建的class对象是我们修改之后的字节码内容,那我们就可以实现一个回调函数赋给JvmtiEnv(JVMTI的运行时,通常一个JVMTIAgent对应一个jvmtiEnv,但是也可以对应多个)的回调方法集合里的ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数里来了,大致实现如下:
jvmtiEventCallbacks callbacks;
jvmtiEnv * jvmtienv = jvmti(agent);
jvmtiError jvmtierror;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
&callbacks,
sizeof(callbacks));
JVMTIAgent
JVMTIAgent其实就是一个动态库,利用JVMTI暴露出来的一些接口来干一些我们想做但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved); JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved); JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
说到javaagent必须要讲的是一个叫做instrument的JVMTIAgent(linux下对应的动态库是libinstrument.so),因为就是它来实现javaagent的功能的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),从这名字里也完全体现了其最本质的功能:就是专门为java语言编写的插桩服务提供支持的。
INSTRUMENT AGENT
instrument agent实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说我们在用它的时候既支持启动的时候来加载agent,也支持在运行期来动态来加载这个agent,其中启动时加载agent还可以通过类似-javaagent:myagent.jar的方式来间接加载instrument agent,运行期动态加载agent依赖的是jvm的attach机制JVM Attach机制实现,通过发送load命令来加载agent。
这里解释下几个重要项:
- mNormalEnvironment:主要提供正常的类transform及redefine功能的。
- mRetransformEnvironment:主要提供类retransform功能的。
- mInstrumentationImpl:这个对象非常重要,也是我们java agent和JVM进行交互的入口,或许写过javaagent的人在写premain以及agentmain方法的时候注意到了有个Instrumentation的参数,这个参数其实就是这里的对象。
- mPremainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallPremain方法,如果agent是在启动的时候加载的,那该方法会被调用。
- mAgentmainCaller:指向sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain方法,该方法在通过attach的方式动态加载agent的时候调用。
- mTransform:指向sun.instrument.InstrumentationImpl.transform方法。
- mAgentClassName:在我们javaagent的MANIFEST.MF里指定的Agent-Class。
- mOptionsString:传给agent的一些参数。
- mRedefineAvailable:是否开启了redefine功能,在javaagent的MANIFEST.MF里设置Can-Redefine-Classes:true。
- mNativeMethodPrefixAvailable:是否支持native方法前缀设置,通样在javaagent的MANIFEST.MF里设置Can-Set-Native-Method-Prefix:true。
- mIsRetransformer:如果在javaagent的MANIFEST.MF文件里定义了Can-Retransform-Classes:true,那将会设置mRetransformEnvironment的mIsRetransformer为true。
红色标注的是我们最常用的,下面的列子也是会用到的...,接下来看一个具体的例子,如果熟悉分布式调用链系统的人肯定知道,调用链中最基础的一个功能就是统计一个服务里面的某个方法执行了多长时间...,其实这个就目前来说大多数系统底层都是基于字节码插桩技术实现的,接下来就演示一个完整的例子....,定义一个业务类,类里面定义几个方法,然后在执行这个方法的时候,会动态实现方法的耗时统计。
看业务类定义:
package com.dxz.chama.service; import java.util.LinkedList;
import java.util.List; /**
* 模拟数据插入服务
*
*/
public class InsertService { public void insert2(int num) {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < num; i++) {
list.add(i);
}
} public void insert1(int num) {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < num; i++) {
list.add(i);
}
} public void insert3(int num) {
List<Integer> list = new LinkedList<>();
for (int i = 0; i < num; i++) {
list.add(i);
}
}
}
删除服务:
package com.dxz.chama.service;
import java.util.List;
public class DeleteService {
public void delete(List<Integer>list){
for (int i=0;i<list.size();i++){
list.remove(i);
}
}
}
ok,接下来就是要编写javaagent的相关实现:
定义agent的入口
package com.dxz.chama.javaagent; import java.lang.instrument.Instrumentation; /**
* agent的入口类
*/
public class TimeMonitorAgent {
// peremain 这个方法名称是固定写法 不能写错或修改
public static void premain(String agentArgs, Instrumentation inst) {
System.out.println("execute insert method interceptor....");
System.out.println(agentArgs);
// 添加自定义类转换器
inst.addTransformer(new TimeMonitorTransformer(agentArgs));
}
}
接下来看最重要的Transformer的实现:
package com.dxz.chama.javaagent; import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import java.util.Objects; import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod; /**
* 类方法的字节码替换
*/
public class TimeMonitorTransformer implements ClassFileTransformer { private static final String START_TIME = "\nlong startTime = System.currentTimeMillis();\n";
private static final String END_TIME = "\nlong endTime = System.currentTimeMillis();\n";
private static final String METHOD_RUTURN_VALUE_VAR = "__time_monitor_result";
private static final String EMPTY = ""; private String classNameKeyword; public TimeMonitorTransformer(String classNameKeyword){
this.classNameKeyword = classNameKeyword;
} /**
*
* @param classLoader 默认类加载器
* @param className 类名的关键字 因为还会进行模糊匹配
* @param classBeingRedefined
* @param protectionDomain
* @param classfileBuffer
* @return
* @throws IllegalClassFormatException
*/
public byte[] transform(ClassLoader classLoader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
className = className.replace("/", ".");
CtClass ctClass = null;
try {
//使用全称,用于取得字节码类
ctClass = ClassPool.getDefault().get(className);
//匹配类的机制是基于类的关键字 这个是客户端传过来的参数 满足就会获取所有的方法 不满足跳过
if(Objects.equals(classNameKeyword, EMPTY)||(!Objects.equals(classNameKeyword, EMPTY)&&className.indexOf(classNameKeyword)!=-1)){
//所有方法
CtMethod[] ctMethods = ctClass.getDeclaredMethods();
//遍历每一个方法
for(CtMethod ctMethod:ctMethods){
//修改方法的字节码
transformMethod(ctMethod, ctClass);
}
}
//重新返回修改后的类
return ctClass.toBytecode();
} catch (Exception e) {
e.printStackTrace();
} return null;
} /**
* 为每一个拦截到的方法 执行一个方法的耗时操作
* @param ctMethod
* @param ctClass
* @throws Exception
*/
private void transformMethod(CtMethod ctMethod,CtClass ctClass) throws Exception{
//抽象的方法是不能修改的 或者方法前面加了final关键字
if((ctMethod.getModifiers()&Modifier.ABSTRACT)>0){
return;
}
//获取原始方法名称
String methodName = ctMethod.getName();
String monitorStr = "\nSystem.out.println(\"method " + ctMethod.getLongName() + " cost:\" +(endTime - startTime) +\"ms.\");";
//实例化新的方法名称
String newMethodName = methodName + "$impl";
//设置新的方法名称
ctMethod.setName(newMethodName);
//创建新的方法,复制原来的方法 ,名字为原来的名字
CtMethod newMethod = CtNewMethod.copy(ctMethod,methodName, ctClass, null); StringBuilder bodyStr = new StringBuilder();
//拼接新的方法内容
bodyStr.append("{"); //返回类型
CtClass returnType = ctMethod.getReturnType(); //是否需要返回
boolean hasReturnValue = (CtClass.voidType != returnType); if (hasReturnValue) {
String returnClass = returnType.getName();
bodyStr.append("\n").append(returnClass + " " + METHOD_RUTURN_VALUE_VAR + ";");
} bodyStr.append(START_TIME); if (hasReturnValue) {
bodyStr.append("\n").append(METHOD_RUTURN_VALUE_VAR + " = ($r)" + newMethodName + "($$);");
} else {
bodyStr.append("\n").append(newMethodName + "($$);");
} bodyStr.append(END_TIME);
bodyStr.append(monitorStr); if (hasReturnValue) {
bodyStr.append("\n").append("return " + METHOD_RUTURN_VALUE_VAR+" ;");
} bodyStr.append("}");
//替换新方法
newMethod.setBody(bodyStr.toString());
//增加新方法
ctClass.addMethod(newMethod);
}
}
其实也很简单就两个类就实现了要实现的功能,那么如何使用呢?需要把上面的代码打成jar包才能执行,建议大家使用maven打包,下面是pom.xml的配置文件
<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>com.dxz</groupId>
<artifactId>chama</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging> <name>chama</name>
<url>http://maven.apache.org</url> <properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> <dependencies>
<dependency>
<groupId>javassist</groupId>
<artifactId>javassist</artifactId>
<version>3.12.1.GA</version>
</dependency> <!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.2.5</version>
</dependency> <!-- https://mvnrepository.com/artifact/oro/oro -->
<dependency>
<groupId>oro</groupId>
<artifactId>oro</artifactId>
<version>2.0.8</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.0.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<manifestEntries>
<Premain-Class>com.dxz.chama.javaagent.TimeMonitorAgent</Premain-Class>
</manifestEntries>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
强调一下,红色标准的非常关键,因为如果要想jar能够运行,必须要把运行清单打包到jar中,且一定要让jar的主类是Permain-Class,否则无法运行,运行清单的目录是这样的.
mvn -clean package
如果打包正确的话,里面的内容应该如下所示:

OK至此整体代码和打包就完成了,那么接下来再讲解如何使用
部署方式:
1 基于IDE开发环境运行
首先,编写一个service的测试类如下:
package com.dxz.chama.service; import java.util.LinkedList;
import java.util.List; public class ServiceTest {
public static void main(String[] args) {
// 插入服务
InsertService insertService = new InsertService();
// 删除服务
DeleteService deleteService = new DeleteService();
System.out.println("....begnin insert....");
insertService.insert1(1003440);
insertService.insert2(2000000);
insertService.insert3(30003203); System.out.println(".....end insert.....");
List<Integer> list = new LinkedList<>();
for (int i = 0; i < 29988440; i++) {
list.add(i);
}
System.out.println(".....begin delete......");
deleteService.delete(list);
System.out.println("......end delete........"); }
}
选择编辑配置:如下截图所示

service是指定要拦截类的关键字,如果这里的参数是InsertService,那么DeleteService相关的方法就无法拦截了。同理也是一样的。
chama-0.0.1-SNAPSHOT.jar这个就是刚刚编写那个javaagent类的代码打成的jar包,ok 让我们看一下最终的效果如何:

实际应用场景中,可以把这些结果写入到log然后发送到es中,就可以做可视化数据分析了...还是蛮强大的,接下来对上面的业务进行扩展,因为上面默认是拦截类里面的所有方法,如果业务需求是拦截类的特定的方法该怎么实现呢?其实很简单就是通过正则匹配,下面给出核心代码:
定义入口agent:
package com.dxz.chama.javaagent.patter;
import java.lang.instrument.Instrumentation; public class TimeMonitorPatterAgent {
public static void premain(String agentArgs, Instrumentation inst) {
inst.addTransformer(new PatternTransformer());
}
}
定义transformer:
package com.dxz.chama.javaagent.patter; import javassist.CtClass;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher; import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain; public class PatternTransformer implements ClassFileTransformer {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
PatternMatcher matcher = new Perl5Matcher();
PatternCompiler compiler = new Perl5Compiler();
// 指定的业务类
String interceptorClass = "com.dxz.chama.service.InsertService";
// 指定的方法
String interceptorMethod = "insert1";
try {
if (matcher.matches(className, compiler.compile(interceptorClass))) {
ByteCode byteCode = new ByteCode();
CtClass ctClass = byteCode.modifyByteCode(interceptorClass, interceptorMethod);
return ctClass.toBytecode();
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
修改字节码的实现:
package com.dxz.chama.javaagent.patter; import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;
import javassist.CtNewMethod; public class ByteCode {
public CtClass modifyByteCode(String className, String method) throws Exception {
ClassPool classPool = ClassPool.getDefault();
CtClass ctClass = classPool.get(className);
CtMethod oldMethod = ctClass.getDeclaredMethod(method);
String oldMethodName = oldMethod.getName();
String newName = oldMethodName + "$impl";
oldMethod.setName(newName); CtMethod newMethod = CtNewMethod.copy(oldMethod, oldMethodName, ctClass, null);
StringBuffer sb = new StringBuffer();
sb.append("{");
sb.append("\nSystem.out.println(\"start to modify bytecode\");\n");
sb.append("long start = System.currentTimeMillis();\n");
sb.append(newName + "($$);\n");
sb.append("System.out.println(\"call method" + oldMethodName + "took\"+(System.currentTimeMillis()-start))");
sb.append("}");
newMethod.setBody(sb.toString());
ctClass.addMethod(newMethod);
return ctClass;
}
}
OK,
修改下pom中的
<manifestEntries>
<Premain-Class>com.dxz.chama.javaagent.patter.TimeMonitorPatterAgent</Premain-Class>
</manifestEntries>
这个时候再重新打包,然后修改上面的运行配置之后再看效果,只能拦截到insert1方法

最后 再说一下如何使用jar运行,其实很简单如下:把各个项目都打成jar,比如把上面的service打成service.jar,然后使用java命令运行:
java -javaagent:d://chama-0.0.1-SNAPSHOT.jar=Service -jar service.jar,效果是一样的!
JVM插码之三:javaagent介绍及javassist介绍的更多相关文章
- JVM插码之四:Java动态代理机制的对比(JDK 和CGLIB,Javassist,ASM)
一.class文件简介及加载 Java编译器编译好Java文件之后,产生.class 文件在磁盘中.这种class文件是二进制文件,内容是只有JVM虚拟机能够识别的机器码.JVM虚拟机读取字节码文件, ...
- JVM插码之六:jacoco插码及问题“$jacocodata 属性 Method not found: is$jacocoData”
在使用jacoco统计自动化代码覆盖率 jacoco统计自动化代码覆盖率 1. 简介1.1. 什么是JacocoJacoco是一个开源的代码覆盖率工具,可以嵌入到Ant .Maven中,并提供了Ecl ...
- JVM插码之五:Java agent+ASM实战--监控所有方法执行时间
本文建立在对instrumentation和agent有初步的了解的前提下阅读,关于这2个类的讲解在其它文章中. 这是一个maven项目,pom中需要的配置,lib中有asm的jar包 pom.xml ...
- 深入浅出Java探针技术2---java字节码生成框架ASM、Javassist和byte buddy的使用
目前Java字节码生成框架大致有ASM.Javassist和byte buddy三种 ASM框架介绍及使用 1.ASM介绍 ASM是一种Java字节码操控框架,能够以二进制形式修改已有的类或是生成类, ...
- JVM插庄之二:Java agent基础原理
javaagent 简介 Javaagent 只要作用在class被加载之前对其加载,插入我们需要添加的字节码. Javaagent面向的是我们java程序员,而且agent都是用java编写的,不需 ...
- 从jvm源码看synchronized
从jvm源码看synchronized 索引 synchronized的使用 修饰实例方法 修饰静态方法 修饰代码块 总结 Synchronzied的底层原理 对象头和内置锁(ObjectMonito ...
- JVM 字节码执行实例分析
前言 最近在看<Java 虚拟机规范>和<深入理解JVM虚拟机>,对于字节码的执行有了进一步的了解.字节码就像是汇编语言,是 JVM 的指令集.下面我们先对 JVM 执行引擎做 ...
- JVM源码分析之SystemGC完全解读
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
- JVM总结(五):JVM字节码执行引擎
JVM字节码执行引擎 运行时栈帧结构 局部变量表 操作数栈 动态连接 方法返回地址 附加信息 方法调用 解析 分派 –“重载”和“重写”的实现 静态分派 动态分派 单分派和多分派 JVM动态分派的实现 ...
随机推荐
- Linux mariadb(Mysql)的主从复制架构
mysql的主从复制架构,需要准备两台机器,并且可以通信,安装好2个mysql,保持版本一致性 mysql -v 查看数据库版本 1.准备主库的配置文件 /etc/my.cnf 写入开启主库的参数[ ...
- Java语言实现简单FTP软件------>辅助功能模块FTP站点管理的实现(十二)
1.FTP站点管理 点击"FTP站点管理"按钮,弹出对话框"FTP站点管理",如下图 1) 连接站点 在FTP站点管理面板上选好要连接的站点,点击"连 ...
- Linux备份和回复mysql数据库
备份:mysqldump -u root -p密码 数据库名>/home/data.bak mysqldump -u root -p密码 数据库名.表名>/home/data.bak ...
- PHP网页导出Word文档的方法分离
今天要探讨的是PHP网页导出Word文档的方法,使用其他语言的朋友也可以参考,因为原理是差不多的. 原理 一般,有2种方法可以导出doc文档,一种是使用com,并且作为php的一个扩展库安装到服务器上 ...
- Android TableLayout 表格布局
TableLayout继承LinearLayout 有多少个TableRow对象就有多少行, 列数等于最多子控件的TableRow的列数 直接在TableLayout加控件,控件会占据一行 Table ...
- 查找mysql的cnf文件位置
mysql --help|grep 'my.cnf' 查看mysql启动时读取配置文件的默认目录 命令 mysql --help|grep 'my.cnf' 输出 order of preferenc ...
- mac活动监视器闪退
输入:rm -rf ~/Library/LaunchAgents/com.apple.Yahoo.plist即可!
- 算法(Algorithms)第4版 练习 1.5.8
假设原id数组: 0 1 1 4 4 8 6 1 8 0 输入p = 5, q = 7 则输出结果会出错,最终为: 0 1 1 4 4 1 6 1 8 0 因为当id[p](id[5] = 8)被赋值 ...
- Java -- 表达式类型的自动提升
1. 提升规则: a. 所有byte型,short型和char型将被提升到int型. b. 整个算数表达式的数据类型自动提升到与表达式中最高等级操作数同样的类型. 例1: short val = 5; ...
- Eclipse debug neutron-server
1 首先停掉neutron-server kill neutron-server in screen by ctr-c q-svc 2 cp /usr/local/bin/neutron-server ...