什么是JavaAgent?

前几天和同学在排查一个线上问题时,发现一个有漏洞的HSF请求,急需对该HSF进行屏蔽,但是发现该系统未接入限流,这下懵逼了。但是一个同学灵机一动,使用了故障演练平台对该接口模拟hsf调用方异常。屏蔽了该请求。顿时对这个平台的技术产生了兴趣。故障演练平台。

粗略查看了它的手册,发现其使用了JavaAgent的技术。那么,什么是JavaAgent呢?

JavaAgent是基于JVMTI实现的。(从jdk1.5.0 版本加入 Java 虚拟机工具接口,用于监控JVM各项信息)。以下是JavaAgent的主要功能。

  • 可以在加载class文件之前做拦截把字节码做修改
  • 可以在运行期将已经加载的类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
  • 还有其他的一些小众的功能
    • 获取所有已经被加载过的类
    • 获取所有已经被初始化过了的类(执行过了clinit方法,是上面的一个子集)
    • 获取某个对象的大小
    • 将某个jar加入到bootstrapclasspath里作为高优先级被bootstrapClassloader加载
    • 将某个jar加入到classpath里供AppClassloard去加载
    • 设置某些native方法的前缀,主要在查找native方法的时候做规则匹配

所以 ,初步认为是故障演练平台使用了mkagent,对hsf接口进行了mock,让其模拟异常。实现接口调用的失败。

如何实现简单的JavaAgent

现阶段实现agent至少有二种方式,

一种是基于JVMTI的“java -agentpath:”

通过加载使用C编译的动态库的方式实现。在linux与mac下动态库是“libname.so”,windows下动态库是“libname.dll”。一般会实现以下三个方法。

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);
  • Agent_OnLoad函数,如果agent是在启动的时候加载的,也就是在vm参数里通过-agentlib来指定,那在启动过程中就会去执行这个agent里的Agent_OnLoad函数。
  • Agent_OnAttach函数,如果agent不是在启动的时候加载的,是我们先attach到目标进程上,然后给对应的目标进程发送load命令来加载agent,在加载过程中就会调用Agent_OnAttach函数。
  • Agent_OnUnload函数,在agent做卸载的时候调用,不过貌似基本上很少实现它。

首先是一段C++的动态库代码,注意头文件jvmti.h与jni_md.h

/*
*  JVMTI agent
*/
#include <jvmti.h>
#include <string>
#include <cstring>
#include <iostream>
#include <list>
#include <map>
#include <set>
#include <stdlib.h>
#include <jni_md.h>JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *jvm, char *options, void *reserved) {printf("Agent_OnAttach success! \n");jvmtiEnv *jvmti;jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);if (result != JNI_OK) {printf("ERROR: Unable to access JVMTI!\n");}jvmtiError err = (jvmtiError) 0;jclass *classes;jint count;err = jvmti->GetLoadedClasses(&count, &classes);//获取classif (err) {printf("ERROR: JVMTI GetLoadedClasses failed!\n");}for (int i = 0; i < count; i++) {char *sig;jvmti->GetClassSignature(classes[i], &sig, NULL);//获取并打印class签名printf("cls sig=%s\n", sig);}return err;
}JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {// nothing to do
}JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) {printf("Agent_OnLoad success! \n");jvmtiError err = (jvmtiError) 0;return err;
}
​

以上代码主要实现了2个方法,Agent_OnAttach是在程序在运行时加载,并打印所有的加载类。Agent_OnLoad是在程序启动时加载,打印一个语句。
然后对这个CPP进行编译。生成一个.so的动态库文件。

g++ -I${JAVA_HOME}/include/ -I${JAVA_HOME}/include/darwin Agent.cpp -fPIC -shared -o libagent.so

然后是被agent的目标的类,并且对其进行编译

public class TestMain {public static void main(String[] args) throws InterruptedException {System.out.println("JVMTI agent Test start");int i = 0;while (i < 100) {Thread.sleep(1000);i++;System.out.println(i);}}
}

然后开始尝试程序启动阶段的agent,使用以下指令进行运行

java -agentpath:/Users/archersblood/Desktop/libagent.so TestMain

可以看到日志文件的输出。

Agent_OnLoad success!
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12​

这是程序在启动的时候进行agent了,jvm执行了动态库中的Agent_OnLoad代码。

另外就是程序运行中的agent了。现在需要以下代码。

import java.io.IOException;import com.sun.tools.attach.AgentInitializationException;
import com.sun.tools.attach.AgentLoadException;
import com.sun.tools.attach.AttachNotSupportedException;
import com.sun.tools.attach.VirtualMachine;public class TestAgent {public static void main(String[] args) throws AttachNotSupportedException, IOException, AgentLoadException,AgentInitializationException {String pid = "1136"; // java进程pidString agentPath = "/Users/archersblood/Desktop/libagent.so"; // agent.so的路径String options = null;// 传入agent的参数VirtualMachine virtualMachine = com.sun.tools.attach.VirtualMachine.attach(pid);virtualMachine.loadAgentPath(agentPath, options);virtualMachine.detach();}
}​

当运行TestMain的main函数的时候,ps aux | grep java 查看TestMain的PID,然后修改以上代码中的pid参数,让Agent加载到指定的java进程中。然后立马运行TestAgent,就会看到以下日志。

66
67
68
69
70
71
72
Agent_OnAttach success!
cls sig=Lagent/TestMain;
cls sig=Lorg/objectweb/asm/MethodWriter;
cls sig=Lorg/objectweb/asm/FieldWriter;
cls sig=[Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/Item;
cls sig=Lorg/objectweb/asm/ByteVector;
cls sig=Lorg/objectweb/asm/FieldVisitor;
cls sig=Lorg/objectweb/asm/MethodVisitor;
cls sig=Lorg/objectweb/asm/AnnotationVisitor;
cls sig=Lorg/objectweb/asm/ClassWriter;
cls sig=Lorg/objectweb/asm/ClassVisitor;
cls sig=Lagent/MyAgent;
cls sig=Ljava/lang/Void;
cls sig=Ljava/lang/Class$MethodArray;
cls sig=Lsun/launcher/LauncherHelper$FXHelper;
cls sig=[Lsun/launcher/LauncherHelper;
cls sig=Lsun/launcher/LauncherHelper;
cls sig=Lsun/usagetracker/UsageTrackerClient$3;
cls sig=Lsun/usagetracker/UsageTrackerClient$4;
cls sig=Lsun/usagetracker/UsageTrackerClient$1;
cls sig=Ljava/util/concurrent/atomic/AtomicBoolean;
cls sig=Lsun/usagetracker/UsageTrackerClient;
cls sig=Lsun/misc/PostVMInitHook;
cls sig=Ljava/lang/invoke/MethodHandleStatics$1;
cls sig=Ljava/lang/invoke/MethodHandleStatics;
cls sig=Ljava/lang/invoke/MemberName$Factory;
cls sig=Ljava/lang/ClassValue$Version;
cls sig=Ljava/lang/ClassValue$Identity;
cls sig=[Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/ClassValue$Entry;
cls sig=Ljava/lang/invoke/MethodHandleImpl$4;
cls sig=Ljava/lang/ClassValue;
cls sig=[Ljava/lang/invoke/MethodHandle;
cls sig=Ljava/lang/invoke/MethodHandleImpl$3;
cls sig=Ljava/lang/invoke/MethodHandleImpl$2;
cls sig=Ljava/util/function/Function;
cls sig=Ljava/lang/invoke/MethodHandleImpl$1;
cls sig=Ljava/lang/invoke/MethodHandleImpl;
cls sig=Ljava/io/FileOutputStream$1;
cls sig=Ljava/lang/IllegalStateException;
cls sig=Ljava/util/concurrent/ConcurrentHashMap$ForwardingNode;
cls sig=Lsun/security/util/ManifestEntryVerifier;
cls sig=Ljava/io/ByteArrayOutputStream;
cls sig=Ljava/util/jar/JarVerifier$3;
cls sig=[Ljava/security/CodeSigner;
cls sig=Ljava/security/CodeSigner;
cls sig=Ljava/util/jar/JarVerifier;
cls sig=Lsun/misc/ASCIICaseInsensitiveComparator;
cls sig=Ljava/util/jar/Attributes$Name;
cls sig=Ljava/util/jar/Manifest$FastInputStream;
cls sig=Ljava/util/jar/Attributes;
cls sig=Lsun/misc/URLClassPath$JarLoader$2;
cls sig=Lsun/misc/IOUtils;
cls sig=Lsun/misc/ExtensionDependency;​

计数器打印到一半时,执行了动态库中的Agent_OnAttach方法。

基于-javaagent的premain方法

以上是使用JVMTI实现的agent。但是故障演练平台应该不是用这种方法。在改平台的手册上看到了这句话:

如图可知,mk修改业务java启动进程脚本(setenv.sh)就是增加了如下一行:
-javaagent:/usr/alisys/dragoon/libexec/monkeyking/mkagent.jar
-Dproject.name=sonar 表示业务java进程是sanar​

先看以下代码

package agent;import java.io.File;
import java.io.FileOutputStream;
import java.lang.instrument.Instrumentation;import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;public class MyAgent {/*** 该方法在main方法之前运行,与main方法运行在同一个JVM中* 并被同一个System ClassLoader装载* 被统一的安全策略(security policy)和上下文(context)管理** @param agentOps* @param inst* @throws Exception */public static void premain(String agentOps, Instrumentation inst) throws Exception {System.out.println("=========premain方法执行========");System.out.println(agentOps);ClassWriter cw = new ClassWriter(0);//通过visit方法确定类的头部信息cw.visit(Opcodes.V1_7, Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT+Opcodes.ACC_INTERFACE,"com/asm3/Comparable", null, "java/lang/Object", new String[]{"com/asm3/Mesurable"});//定义类的属性cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,"LESS", "I", null, new Integer(-1)).visitEnd();cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,"EQUAL", "I", null, new Integer(0)).visitEnd();cw.visitField(Opcodes.ACC_PUBLIC+Opcodes.ACC_FINAL+Opcodes.ACC_STATIC,"GREATER", "I", null, new Integer(1)).visitEnd();//定义类的方法cw.visitMethod(Opcodes.ACC_PUBLIC+Opcodes.ACC_ABSTRACT, "compareTo","(Ljava/lang/Object;)I", null, null).visitEnd();cw.visitEnd(); //使cw类已经完成//将cw转换成字节数组写到文件里面去byte[] data = cw.toByteArray();File file = new File("/Users/archersblood/Desktop/test.class");FileOutputStream fout = new FileOutputStream(file);fout.write(data);fout.close();}/*** 如果不存在 premain(String agentOps, Instrumentation inst) * 则会执行 premain(String agentOps)** @param agentOps* @author */public static void premain(String agentOps) {System.out.println("=========premain方法执行2========");System.out.println(agentOps);}}​

该代码使用了javaagent与ASM(Java字节码操纵框架)技术,作用是在程序运行前生成一个com.asm3.Comparable接口,该agent默认是在程序main运行之前,执行premain方法。
除了该段代码,还要在该类包接口下创建META-INF文件夹,并建立MANIFEST.MF文件,以下为文件内容:

Manifest-Version: 1.0
Premain-Class: agent.MyAgent
Can-Redefine-Classes: true​

指定agent.MyAgent为PremainClass。同时将MyAgent与MF文件打包,我使用的是eclipse导出。然后使用以下指令(可以参考故障演练平台的配置)

java -javaagent:/Users/archersblood/Desktop/myAgent.jar TestMain​
=========premain方法执行========
null
JVMTI agent Test start
1
2
3
4
5
6
7
8
9
10
11
12
13

最后,生成了一个test.class。对其反编译

javap -c test.class >test.txt   ​

打开该文件,就是利用asm字节码操作生成的一个接口

public interface com.asm3.Comparable extends com.asm3.Mesurable {public static final int LESS;public static final int EQUAL;public static final int GREATER;public abstract int compareTo(java.lang.Object);
}​

推测,可能故障演练平台上就是使用的就是javaagent+asm技术,在应用启动或运行时,通过修改类的字节码,模拟系统的各种故障。(以上都是瞎猜,猜错了我不负责,演练平台的同学别找我^o^)

PS:文章中的代码大多来自于网络,但本人亲测有效。

JavaAgent学习笔记相关推荐

  1. Spring5底层原理 学习笔记(二)AOP篇

    文章目录 AOP实现之ajc编译器 AOP实现之agent类加载 AOP实现之动态代理 jdk动态代理 演示 模拟实现动态代理 动态生成代理类需要使用到asm的api,这里就不展开了 Jdk对于反射调 ...

  2. jmh学习笔记-State共享对象

    系列文章目录 jmh学习笔记-源代码编译与bench mode jmh学习笔记-State共享对象 jmh学习笔记-State共享对象前后置方法 jmh学习笔记-代码清除 jmh学习笔记-常量折叠 j ...

  3. Mybatis的学习笔记

    MyBatis Mybatis是一款非常优秀的持久层框架,学习的目的是要掌握精通. 那么今天我们就来学习一下这个优秀的框架知识! 此外,由于博主的水平的原因,文章的质量可能不会太高,请酌情观看.如果大 ...

  4. PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 call

    您的位置 首页 PyTorch 学习笔记系列 PyTorch 学习笔记(六):PyTorch hook 和关于 PyTorch backward 过程的理解 发布: 2017年8月4日 7,195阅读 ...

  5. 容器云原生DevOps学习笔记——第三期:从零搭建CI/CD系统标准化交付流程

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  6. 容器云原生DevOps学习笔记——第二期:如何快速高质量的应用容器化迁移

    暑期实习期间,所在的技术中台-效能研发团队规划设计并结合公司开源协同实现符合DevOps理念的研发工具平台,实现研发过程自动化.标准化: 实习期间对DevOps的理解一直懵懵懂懂,最近观看了阿里专家带 ...

  7. 2020年Yann Lecun深度学习笔记(下)

    2020年Yann Lecun深度学习笔记(下)

  8. 2020年Yann Lecun深度学习笔记(上)

    2020年Yann Lecun深度学习笔记(上)

  9. 知识图谱学习笔记(1)

    知识图谱学习笔记第一部分,包含RDF介绍,以及Jena RDF API使用 知识图谱的基石:RDF RDF(Resource Description Framework),即资源描述框架,其本质是一个 ...

最新文章

  1. CentOS 7 yum 安装 MySQL5.7
  2. php基类是什么意思,php中的方法重写是什么意思?
  3. 白话Elasticsearch66-针对集群重启时的shard恢复耗时过长问题定制的重要参数
  4. 基于Orangpi Zero和Linux ALSA实现WIFI无线音箱(三)
  5. 去除inline-block元素间间距
  6. 【JUnit】BeforeClass、AfterClass、Before与After示例
  7. 机器学习必备的数学知识,一次学会
  8. IDEA中识别Git上新加的分支
  9. [转]STL(容器)与DEBUGNEW运算符冲突的解决
  10. 【NodeJs】用WebStorm创建Express项目时出现:express version is unavailable
  11. 如何根据MAC地址获取设备厂商
  12. 傅里叶变换公式整理,意义和定义,概念及推导
  13. Speedoffice(word)怎么撤回上一步的操作
  14. Tuxera NTFS如何解决硬盘无法写入文件教程分享
  15. 没有一个程序员,能“活过”40岁
  16. Chrome插件开发-右键菜单开启页面编辑
  17. Linux的numactl
  18. 梦想照进现实|CSDN 实体奖牌 第二期
  19. 2018国内各大互联网公司前端面试题汇总【转:公众号~~高级前端进阶公众号】
  20. 一阶RC低通滤波电路数字化

热门文章

  1. 分分钟使用Retrofit+Rxjava实现网络请求
  2. JSP中文件上传的关键步骤
  3. 报此错错解决办法:java.lang.NoSuchMethodError: javax.persistence.OneToMany.orphanRemoval()Z
  4. Tomcat6.0配置集合
  5. DotNet Framework 小技巧
  6. java jetty的classpath_java代码启动jetty
  7. hdfs 多租户_Hadoop多租户架构配置
  8. HTTP GET/CONNECT代理区别
  9. 《Android Design》 4.4 中文版
  10. CoreAnimation-CATransaction